2bf6701def
执行机制四道闸 → 闸4(CI 硬闸) + 评审兜底落地: - ci/scan-portable-sql.sh:扫 server Go 运行时查询禁用 MySQL 专属构造 (UTC_TIMESTAMP/UTC_DATE/NOW()/FIELD(/ON DUPLICATE KEY/IFNULL), 豁免方言层 dialect*.go / _test.go / 注释行;当前 152 文件 0 违规 - ci.yml:新增 portable-sql-scan job;push 触发路径补 server/** - .gitea/PULL_REQUEST_TEMPLATE.md:可测试性五支柱 + 测试 + 红线自检清单 - 守支柱 3(纯逻辑/IO 分离·双库可移植),见 docs/dev-conventions.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.9 KiB
Bash
Executable File
79 lines
2.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scan-portable-sql.sh — 可移植 SQL 扫描(支柱 3:纯逻辑/IO 分离 → 双库可移植)。
|
|
#
|
|
# 规则来源: CLAUDE.md「数据层:多数据库」铁律——server 已与具体 DB 解绑(裸 SQL +
|
|
# 薄方言层 internal/db/dialect.go)。Go 端运行时查询不得使用 MySQL 专属构造,因为
|
|
# 同一份 SQL 要同时跑 mysql 与 sqlite。可移植写法:
|
|
# - upsert → dialect.Upsert(...)(中性 EXCLUDED.col 记法)
|
|
# - 行锁 → dialect.LockForUpdate()
|
|
# - 时间 → Go 端算好传 ?(不用 UTC_TIMESTAMP()/NOW()/UTC_DATE())
|
|
#
|
|
# 扫描范围: server/**/*.go(运行时代码)
|
|
# 白名单(豁免):
|
|
# *_test.go ← 测试 fixture(含 integration-only mysql SQL)
|
|
# internal/db/dialect*.go ← 方言层本身,负责生成各库方言,允许出现专属 SQL
|
|
# 整行注释(// 或 /* 或 *) ← 文档/说明,非实际查询
|
|
#
|
|
# 禁用构造(出现在非注释、非白名单的 Go 代码即违规):
|
|
# UTC_TIMESTAMP UTC_DATE NOW() FIELD( ON DUPLICATE KEY IFNULL(
|
|
#
|
|
# 发现违规 → 非零退出码(CI fail)。
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
VIOLATIONS=0
|
|
SCANNED=0
|
|
|
|
# MySQL 专属构造(可移植替代见 dialect.go / Go 端算时间 / COALESCE)
|
|
PATTERNS='UTC_TIMESTAMP|UTC_DATE|NOW\(\)|FIELD\(|ON DUPLICATE KEY|IFNULL\('
|
|
|
|
# scan_file <文件路径> — 命中返回 1(违规),干净返回 0。
|
|
scan_file() {
|
|
local file="$1"
|
|
local hits
|
|
# 1. 找含禁用构造的行
|
|
# 2. 排除整行注释(行首可选空白 + // 或 /* 或 *)
|
|
hits="$(grep -nE "$PATTERNS" "$file" 2>/dev/null \
|
|
| grep -vE '^[0-9]+:[[:space:]]*(//|/\*|\*)' \
|
|
|| true)"
|
|
|
|
if [ -n "$hits" ]; then
|
|
echo "❌ $file" >&2
|
|
echo "$hits" | while IFS= read -r line; do
|
|
printf ' 不可移植 SQL %s\n' "$line" >&2
|
|
done
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
echo "→ 开始可移植 SQL 扫描(server Go 运行时查询)..."
|
|
|
|
if [ -d server ]; then
|
|
while IFS= read -r -d '' f; do
|
|
SCANNED=$((SCANNED + 1))
|
|
if ! scan_file "$f"; then
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
fi
|
|
done < <(find server -type f -name '*.go' \
|
|
! -name '*_test.go' \
|
|
! -path '*/internal/db/dialect*.go' \
|
|
! -path '*/vendor/*' -print0)
|
|
fi
|
|
|
|
echo "→ 扫描完成:共 ${SCANNED} 个 Go 文件,${VIOLATIONS} 个违规。"
|
|
echo ""
|
|
|
|
if [ "$VIOLATIONS" -ne 0 ]; then
|
|
echo "可移植 SQL 检查失败!同一份 Go SQL 要同时跑 mysql 与 sqlite,请改用可移植写法:" >&2
|
|
echo " - upsert → dialect.Upsert(...)(中性 EXCLUDED.col 记法)" >&2
|
|
echo " - 行锁 → dialect.LockForUpdate()" >&2
|
|
echo " - 时间 → Go 端算好传 ?,不用 UTC_TIMESTAMP()/NOW()/UTC_DATE()" >&2
|
|
echo " - 排序 → 不用 FIELD();空值合并用 COALESCE 不用 IFNULL" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "✅ 可移植 SQL 扫描通过 — server 运行时查询无 MySQL 专属构造。"
|