优化
This commit is contained in:
+16
-1
@@ -44,9 +44,24 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
|
||||
|
||||
## 现有相关脚本与边界
|
||||
|
||||
### 数据库中文注释审计
|
||||
|
||||
`database-comment-audit.ps1` 离线扫描各模块 `src/main/resources/db/*.sql` 中的 `CREATE TABLE` 定义,不连接数据库、不执行 SQL,也不修改任何文件。
|
||||
|
||||
```powershell
|
||||
.\scripts\database-comment-audit.ps1
|
||||
.\scripts\database-comment-audit.ps1 -Modules finance,order
|
||||
.\scripts\database-comment-audit.ps1 -Modules finance -SummaryOnly
|
||||
.\scripts\database-comment-audit.ps1 -Format Json
|
||||
.\scripts\database-comment-audit.ps1 -FailOnIssues
|
||||
.\scripts\database-comment-audit.ps1 -SelfTest
|
||||
```
|
||||
|
||||
默认报告模式即使存在存量缺口也返回 `0`;CI 使用 `-FailOnIssues`,缺失或纯英文表/字段注释返回 `1`,模块或路径配置错误返回 `2`。规范和注释口径见 `../docs/数据库注释维护规范.md`。
|
||||
|
||||
### 本地 MySQL 全量验收
|
||||
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0` 和 7803 行。
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `7F61C3BAB0D4D49525A30C1E5030DEA4B798F38F6A0287901E6D1D676967DFFA` 和 13712 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
|
||||
```powershell
|
||||
.\local-db-full-acceptance.ps1 -DryRun
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
[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
|
||||
@@ -11,8 +11,8 @@ $ErrorActionPreference = 'Stop'
|
||||
$Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$Workspace = (Resolve-Path (Join-Path $Repo '..')).Path
|
||||
$Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance'
|
||||
$ExpectedSha256 = 'E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0'
|
||||
$ExpectedLines = 7803
|
||||
$ExpectedSha256 = '03234DF522426213FA64768FDF35182DC315F2E358E5FD993AA24A2E99936D63'
|
||||
$ExpectedLines = 13775
|
||||
|
||||
if ($TargetHost -notin @('127.0.0.1', 'localhost')) {
|
||||
[Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.')
|
||||
@@ -32,6 +32,25 @@ if ($actual -ne $ExpectedSha256 -or $lines -ne $ExpectedLines) {
|
||||
[Console]::Error.WriteLine("Unified SQL snapshot mismatch: $actual/$lines")
|
||||
exit 2
|
||||
}
|
||||
$sqlText = [System.IO.File]::ReadAllText($InputPath)
|
||||
$commentGateTokens = @(
|
||||
'CREATE PROCEDURE queue_column_comment',
|
||||
'CREATE PROCEDURE apply_one_table_comments',
|
||||
'CALL apply_queued_comments();',
|
||||
'字段注释批量升级改变了结构签名',
|
||||
'存在缺失中文注释的业务表',
|
||||
'存在缺失中文注释的业务字段'
|
||||
)
|
||||
foreach ($token in $commentGateTokens) {
|
||||
if (-not $sqlText.Contains($token)) {
|
||||
[Console]::Error.WriteLine("Unified SQL comment acceptance marker missing: $token")
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
if ([regex]::Matches($sqlText, "SELECT 'PASS' AS migration_status").Count -ne 1) {
|
||||
[Console]::Error.WriteLine('Unified SQL must contain exactly one final PASS statement')
|
||||
exit 2
|
||||
}
|
||||
|
||||
$sequence = @(
|
||||
'empty: base.sql', 'replay: base.sql + input.sql + input.sql',
|
||||
@@ -40,11 +59,20 @@ $sequence = @(
|
||||
'overseas-missing-objects: fixture-overseas-missing-objects.sql',
|
||||
'interrupted-recovery: fixture-interrupted.sql', 'final-structure: verify-final.sql')
|
||||
Write-Output "SQL SHA-256: $actual / lines: $lines"
|
||||
Write-Output 'Comment gate: queued validation, per-table ALTER, signature verification, nine-schema coverage, unique PASS'
|
||||
Write-Output "Target: $TargetHost`:$Port"
|
||||
Write-Output 'Fixture order:'
|
||||
$sequence | ForEach-Object { Write-Output " $_" }
|
||||
if ($DryRun) { Write-Output 'DRY-RUN PASS (mysql not started; no database writes)'; exit 0 }
|
||||
|
||||
$acceptanceInput = Join-Path $Acceptance 'input.sql'
|
||||
Copy-Item -LiteralPath $InputPath -Destination $acceptanceInput -Force
|
||||
$stagedHash = ((Get-FileHash -Algorithm SHA256 -LiteralPath $acceptanceInput).Hash).Trim().ToUpperInvariant()
|
||||
if ($stagedHash -ne $actual) {
|
||||
[Console]::Error.WriteLine("Failed to stage the validated unified SQL snapshot: $stagedHash")
|
||||
exit 2
|
||||
}
|
||||
|
||||
$run = 0
|
||||
function Invoke-Stage([string]$Name, [string]$File) {
|
||||
$script = Join-Path $Acceptance $File
|
||||
|
||||
Reference in New Issue
Block a user