231 lines
8.9 KiB
PowerShell
231 lines
8.9 KiB
PowerShell
[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
|