#!/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 专属构造。"