验收脚本、工具整理与排查报告
- scripts/verify.sh:把「带齐六个环境变量、且必须 0 skip」这条纪律固化下来 * DSN 自己从 conf/app-test.ini 取,不把口令写进脚本 * 库名不含 test 就拒跑(指到维护库会多出「users 不存在」+ 一堆假 skip) * 数 SKIP,非 0 直接失败 —— 不带环境变量时 go test 依然回 ok,最容易被当成通过 * 支持 --fast / --race / --cover / --pkg / --run - tools/:删掉 6 个一次性探针(git 历史里可翻)和误入库的 __pycache__, 只留在用的 4 个工具并补 README;.gitignore 补 __pycache__ / *.pyc - audit-2026-09-21.md:全量排查报告(含 11 批修复记录与收尾审查)
This commit is contained in:
@@ -23,3 +23,5 @@
|
||||
|
||||
# 开发期的临时探针/抓取产物(不属于运行时)
|
||||
/tmpfog/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
+1013
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 一键验收:把「必须带六个环境变量、且必须 0 skip」这条纪律固化下来。
|
||||
#
|
||||
# ./scripts/verify.sh # 全量验收(默认,约 8 分钟)
|
||||
# ./scripts/verify.sh --fast # 只做 gofmt / vet / build(约 20 秒)
|
||||
# ./scripts/verify.sh --race # 全量 + -race(更慢,需 mingw 在 PATH)
|
||||
# ./scripts/verify.sh --cover # 全量 + 覆盖率
|
||||
# ./scripts/verify.sh --pkg ./internal/web/ --run 'TestMapPreview' # 定点跑
|
||||
#
|
||||
# 退出码:0 = 全绿且 0 skip;非 0 = 有失败或有跳过。
|
||||
|
||||
set -u -o pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ABS="$ROOT"
|
||||
command -v pwd >/dev/null && ABS="$(pwd -W 2>/dev/null || pwd)"
|
||||
|
||||
CONF="${ROND_TEST_CONF:-conf/app-test.ini}"
|
||||
ROND_SRC="${ROND_SRC:-$ABS/data/20260914_151649.rondbackup}"
|
||||
FOG_SRC="${FOG_SRC:-$ABS/data/Snapshot-20260914T154839+0800.fwss}"
|
||||
OUT="$ABS/storage/.verify"
|
||||
CLEAN_OUT="$OUT/acceptance-clean.rondbackup"
|
||||
LOG="$OUT/verify.log"
|
||||
|
||||
FAST=0; RACE=0; COVER=0; PKGS="./..."; RUN=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--fast) FAST=1 ;;
|
||||
--race) RACE=1 ;;
|
||||
--cover) COVER=1 ;;
|
||||
--pkg) PKGS="${2:?--pkg 后面要给包路径}"; shift ;;
|
||||
--run) RUN="${2:?--run 后面要给正则}"; shift ;;
|
||||
*) echo "未知参数:$1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
fail() { echo "✗ $*" >&2; exit 1; }
|
||||
ok() { echo "✓ $*"; }
|
||||
|
||||
echo "== 仓库:$ROOT"
|
||||
|
||||
if [ "$FAST" = 1 ]; then
|
||||
unformatted="$(gofmt -l internal/ main.go 2>&1)"
|
||||
[ -n "$unformatted" ] && fail "gofmt 不干净(注意 CRLF 会假警):$(echo "$unformatted" | tr '\n' ' ')"
|
||||
ok "gofmt"
|
||||
go vet ./... || fail "go vet"
|
||||
ok "go vet"
|
||||
go build ./... || fail "go build"
|
||||
ok "go build"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 六个环境变量 ---------------------------------------------------------------
|
||||
[ -f "$CONF" ] || fail "找不到 $CONF"
|
||||
[ -f "$ROND_SRC" ] || fail "找不到备份包 ROND_SRC=$ROND_SRC"
|
||||
[ -f "$FOG_SRC" ] || fail "找不到迷雾快照 FOG_SRC=$FOG_SRC"
|
||||
|
||||
if [ -n "${ROND_DSN:-}" ]; then
|
||||
DSN="$ROND_DSN"
|
||||
else
|
||||
DSN="$(grep -m1 -iE '^[[:space:]]*dsn[[:space:]]*=' "$CONF" | sed 's/^[^=]*=[[:space:]]*//' | tr -d '\r\n')"
|
||||
fi
|
||||
[ -n "$DSN" ] || fail "没能从 $CONF 取到 dsn(也不在环境里),别把口令写进脚本"
|
||||
|
||||
# 指到维护库 postgres 会多出「users 不存在」+ 一堆 stats 假 skip,直接拦住
|
||||
DBNAME="$(printf '%s' "$DSN" | sed -E 's#^[^/]*//[^/]*/([^/?]+).*#\1#')"
|
||||
case "$DBNAME" in
|
||||
test|*test*) : ;;
|
||||
*) fail "ROND_DSN 指向的库名是 '$DBNAME',不含 test —— 验收必须用应用测试库(rond_fog_test)" ;;
|
||||
esac
|
||||
|
||||
export ROND_TEST_CONF="$CONF" ROND_SRC FOG_SRC ROND_DSN="$DSN"
|
||||
export ROND_CLEAN_OUT="$CLEAN_OUT" ROND_EXPORT="$CLEAN_OUT"
|
||||
|
||||
mkdir -p "$OUT"
|
||||
echo "== 库:$DBNAME 备份:$(basename "$ROND_SRC") / $(basename "$FOG_SRC")"
|
||||
|
||||
GOFLAGS_EXTRA=""
|
||||
[ "$RACE" = 1 ] && GOFLAGS_EXTRA="$GOFLAGS_EXTRA -race"
|
||||
[ "$COVER" = 1 ] && GOFLAGS_EXTRA="$GOFLAGS_EXTRA -cover"
|
||||
|
||||
if [ "$RACE" = 1 ] && command -v gcc >/dev/null 2>&1; then :; else
|
||||
for d in "$HOME/scoop/apps/mingw/current/bin"; do
|
||||
[ -d "$d" ] && export PATH="$d:$PATH"
|
||||
done
|
||||
fi
|
||||
if [ "$RACE" = 1 ] && ! command -v gcc >/dev/null 2>&1; then
|
||||
fail "-race 需要 C 编译器:scoop install mingw,或把 mingw 的 bin 加进 PATH"
|
||||
fi
|
||||
|
||||
# --- 第一段:先产出一份「纯净往返包」,第二段要读它 ------------------------------
|
||||
if [ -z "$RUN" ] && [ "$PKGS" = "./..." ]; then
|
||||
echo "== 第一段:导出纯净往返包"
|
||||
if ! go test -count=1 -run '^TestMovementCleanExport$' -v ./internal/web/ 2>&1 | tee "$OUT/stage1.log" | grep -E '^(--- (PASS|FAIL|SKIP)|ok|FAIL)'; then
|
||||
fail "第一段导出失败,见 $OUT/stage1.log"
|
||||
fi
|
||||
grep -q '^--- PASS' "$OUT/stage1.log" || fail "TestMovementCleanExport 没跑起来(被 skip 了?)"
|
||||
ok "纯净往返包已生成:$(basename "$CLEAN_OUT")"
|
||||
fi
|
||||
|
||||
# --- 第二段:全量(或定点)------------------------------------------------------
|
||||
RUNARG=""
|
||||
[ -n "$RUN" ] && RUNARG="-run $RUN"
|
||||
echo "== 第二段:go test -count=1 -v $GOFLAGS_EXTRA $RUNARG $PKGS"
|
||||
# shellcheck disable=SC2086
|
||||
go test -count=1 -v $GOFLAGS_EXTRA -timeout 2400s $RUNARG $PKGS >"$LOG" 2>&1
|
||||
RC=$?
|
||||
|
||||
PASS=$(grep -c '^--- PASS' "$LOG" || true)
|
||||
FAILN=$(grep -c '^--- FAIL' "$LOG" || true)
|
||||
SKIP=$(grep -c '^--- SKIP' "$LOG" || true)
|
||||
BADPKG=$(grep -c '^FAIL' "$LOG" || true)
|
||||
|
||||
echo "== 结果:PASS=$PASS FAIL=$FAILN SKIP=$SKIP 非ok包=$BADPKG"
|
||||
echo "== 日志:$LOG"
|
||||
|
||||
if [ "$SKIP" != 0 ]; then
|
||||
echo "-- 以下用例被跳过(=没跑。门控测试缺环境变量时 go test 依然回 ok,别当通过看)--"
|
||||
grep -n -B1 '^--- SKIP' "$LOG" | head -40
|
||||
fi
|
||||
if [ "$FAILN" != 0 ] || [ "$BADPKG" != 0 ]; then
|
||||
echo "-- 失败明细(前 40 行)--"
|
||||
grep -n -B2 '^--- FAIL' "$LOG" | head -40
|
||||
fi
|
||||
|
||||
if [ "$RC" != 0 ] || [ "$FAILN" != 0 ] || [ "$BADPKG" != 0 ]; then
|
||||
fail "有失败"
|
||||
fi
|
||||
if [ "$SKIP" != 0 ]; then
|
||||
fail "有 $SKIP 个用例被跳过 ⇒ 这次不算验收过"
|
||||
fi
|
||||
ok "全绿且 0 skip(PASS=$PASS)"
|
||||
@@ -0,0 +1,20 @@
|
||||
# tools
|
||||
|
||||
留在这里的都是**还在用**的工具;一次性探针用完就删(历史里能翻到)。
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `verify_export_structure.py` | 结构级对比:导出的 `.rondbackup` / `.fwss` 与真机原版比表、列、行数、悬空引用。导出改动后必跑。 |
|
||||
| `verify_rondbackup.py` | 逐字段对比两个 `.rondbackup`(含 `Z_METADATA` 等字节哈希),比上面那个更细。 |
|
||||
| `photo_coord_audit.py` | 照片导入的坐标体检与批量换算(GCJ-02 / WGS-84)。 |
|
||||
| `audit_db.sql` | 库内一致性审计:`psql "<dsn>" -f tools/audit_db.sql`。 |
|
||||
|
||||
## 验收
|
||||
|
||||
`../scripts/verify.sh`(封了六个环境变量,会数 SKIP,拒绝假绿):
|
||||
|
||||
```bash
|
||||
./scripts/verify.sh # 全量,0 skip 才算过
|
||||
./scripts/verify.sh --fast # gofmt + vet + build
|
||||
./scripts/verify.sh --race --pkg ./internal/web/
|
||||
```
|
||||
Binary file not shown.
@@ -1,89 +0,0 @@
|
||||
// 一次性探针:验证 ZLOCATION.ZPLACEID / ZVISIT.ZEVENTIDENTIFIER 是否跨备份稳定。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
type snap struct {
|
||||
placeID map[string]int
|
||||
eventID map[string]int
|
||||
nulls int
|
||||
total int
|
||||
evTotal int
|
||||
}
|
||||
|
||||
func load(p string) *snap {
|
||||
dir, err := os.MkdirTemp("", "colprobe")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
db, err := rond.Open(p, dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
s := &snap{placeID: map[string]int{}, eventID: map[string]int{}}
|
||||
|
||||
rows, err := db.Query(`SELECT Z_PK, COALESCE(ZPLACEID,'') FROM ZLOCATION`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var id string
|
||||
rows.Scan(&pk, &id)
|
||||
s.total++
|
||||
if id == "" {
|
||||
s.nulls++
|
||||
} else {
|
||||
s.placeID[id] = pk
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rows, err = db.Query(`SELECT COALESCE(ZEVENTIDENTIFIER,'') FROM ZVISIT`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
if id != "" {
|
||||
s.eventID[id] = 1
|
||||
s.evTotal++
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
fmt.Printf("%s: ZLOCATION %d 个(ZPLACEID 空 %d),ZEVENTIDENTIFIER 非空 %d\n",
|
||||
p, s.total, s.nulls, s.evTotal)
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
a := load(os.Args[1])
|
||||
b := load(os.Args[2])
|
||||
inter, samePK := 0, 0
|
||||
for k, pa := range a.placeID {
|
||||
if pb, ok := b.placeID[k]; ok {
|
||||
inter++
|
||||
if pa == pb {
|
||||
samePK++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\nZPLACEID 交集 %d(A 独有 %d,B 独有 %d),其中 Z_PK 相同 %d\n",
|
||||
inter, len(a.placeID)-inter, len(b.placeID)-inter, samePK)
|
||||
|
||||
ei := 0
|
||||
for k := range a.eventID {
|
||||
if _, ok := b.eventID[k]; ok {
|
||||
ei++
|
||||
}
|
||||
}
|
||||
fmt.Printf("ZEVENTIDENTIFIER 交集 %d(A %d / B %d)\n", ei, len(a.eventID), len(b.eventID))
|
||||
}
|
||||
@@ -1,438 +0,0 @@
|
||||
// 一次性探针:把一份 .rondbackup 与 PG 里某个数据集的记录逐条对齐,输出差异画像。
|
||||
// 用完即可删,不属于站点功能。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("用法: diffprobe <archive.rondbackup> <dsn> <datasetID>")
|
||||
os.Exit(2)
|
||||
}
|
||||
archive, dsn := os.Args[1], os.Args[2]
|
||||
var datasetID int64
|
||||
fmt.Sscanf(os.Args[3], "%d", &datasetID)
|
||||
|
||||
tmp, err := os.MkdirTemp("", "diffprobe")
|
||||
must(err)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
sqlDB, err := rond.Open(archive, tmp)
|
||||
must(err)
|
||||
b, err := rond.Parse(sqlDB)
|
||||
sqlDB.Close()
|
||||
must(err)
|
||||
|
||||
pg, err := sql.Open("pgx", dsn)
|
||||
must(err)
|
||||
defer pg.Close()
|
||||
ctx := context.Background()
|
||||
must(pg.PingContext(ctx))
|
||||
|
||||
fmt.Printf("=== 备份 %s ===\n", archive)
|
||||
fmt.Printf("备份: 到访 %d / 地点 %d / 位移 %d / 天气 %d / 活动 %d / 标签 %d / 原始定位 %d\n",
|
||||
len(b.Visits), len(b.Locations), len(b.Movements), len(b.Weather),
|
||||
len(b.Activities), len(b.Tags), b.RawVisitNum)
|
||||
var bFirst, bLast sql.NullTime
|
||||
for _, v := range b.Visits {
|
||||
if v.Arrival == nil {
|
||||
continue
|
||||
}
|
||||
if !bFirst.Valid || v.Arrival.Before(bFirst.Time) {
|
||||
bFirst = sql.NullTime{Time: *v.Arrival, Valid: true}
|
||||
}
|
||||
if !bLast.Valid || v.Arrival.After(bLast.Time) {
|
||||
bLast = sql.NullTime{Time: *v.Arrival, Valid: true}
|
||||
}
|
||||
}
|
||||
fmt.Printf("备份时间跨度: %s ~ %s\n", ts(bFirst), ts(bLast))
|
||||
|
||||
fmt.Printf("\n=== 库内 dataset #%d ===\n", datasetID)
|
||||
var name string
|
||||
var cnt, pcnt, raw int
|
||||
var gFirst, gLast sql.NullTime
|
||||
must(pg.QueryRowContext(ctx, `SELECT original_name, visit_count, place_count, raw_visit_count,
|
||||
first_visit_at, last_visit_at FROM datasets WHERE id=$1`, datasetID).
|
||||
Scan(&name, &cnt, &pcnt, &raw, &gFirst, &gLast))
|
||||
fmt.Printf("%s: 到访 %d / 地点 %d / 原始定位 %d,跨度 %s ~ %s\n", name, cnt, pcnt, raw, ts(gFirst), ts(gLast))
|
||||
|
||||
diffVisits(ctx, pg, datasetID, b)
|
||||
diffPlaces(ctx, pg, datasetID, b)
|
||||
diffMovements(ctx, pg, datasetID, b)
|
||||
diffWeather(ctx, pg, datasetID, b)
|
||||
diffActivities(ctx, pg, datasetID, b)
|
||||
diffTags(ctx, pg, datasetID, b)
|
||||
}
|
||||
|
||||
func ts(t sql.NullTime) string {
|
||||
if !t.Valid {
|
||||
return "-"
|
||||
}
|
||||
return t.Time.Local().Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
type visitRow struct {
|
||||
pk, placeSrc, actSrc int
|
||||
arrival, departure *time.Time
|
||||
remark, emoji string
|
||||
bookmarked, userAdd bool
|
||||
}
|
||||
|
||||
func diffVisits(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
fmt.Printf("\n---------- 到访 ----------\n")
|
||||
rows, err := pg.QueryContext(ctx, `SELECT v.src_pk, COALESCE(p.src_pk,0), COALESCE(a.src_pk,0),
|
||||
v.arrival, v.departure, COALESCE(v.remark,''), COALESCE(v.emoji,''), v.bookmarked, v.is_user_added
|
||||
FROM visits v LEFT JOIN places p ON p.id=v.place_id LEFT JOIN activities a ON a.id=v.activity_id
|
||||
WHERE v.dataset_id=$1`, dsID)
|
||||
must(err)
|
||||
db := map[int]visitRow{}
|
||||
for rows.Next() {
|
||||
var r visitRow
|
||||
var arr time.Time
|
||||
var dep sql.NullTime
|
||||
must(rows.Scan(&r.pk, &r.placeSrc, &r.actSrc, &arr, &dep, &r.remark, &r.emoji, &r.bookmarked, &r.userAdd))
|
||||
r.arrival = &arr
|
||||
if dep.Valid {
|
||||
r.departure = &dep.Time
|
||||
}
|
||||
db[r.pk] = r
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
bk := map[int]rond.Visit{}
|
||||
for _, v := range b.Visits {
|
||||
bk[v.SrcPK] = v
|
||||
}
|
||||
|
||||
var onlyBackup, onlyDB, bothDiff, same []int
|
||||
for pk, v := range bk {
|
||||
r, ok := db[pk]
|
||||
if !ok {
|
||||
onlyBackup = append(onlyBackup, pk)
|
||||
continue
|
||||
}
|
||||
if vdiff(v, r) != "" {
|
||||
bothDiff = append(bothDiff, pk)
|
||||
continue
|
||||
}
|
||||
same = append(same, pk)
|
||||
}
|
||||
for pk := range db {
|
||||
if _, ok := bk[pk]; !ok {
|
||||
onlyDB = append(onlyDB, pk)
|
||||
}
|
||||
}
|
||||
sort.Ints(onlyBackup)
|
||||
sort.Ints(onlyDB)
|
||||
sort.Ints(bothDiff)
|
||||
|
||||
fmt.Printf("同号且一致: %d\n", len(same))
|
||||
fmt.Printf("仅备份有(实机新增,库里没有): %d\n", len(onlyBackup))
|
||||
fmt.Printf("仅库里有(站点侧新增/实机已删除): %d\n", len(onlyDB))
|
||||
fmt.Printf("同号但内容不同(冲突): %d\n", len(bothDiff))
|
||||
|
||||
// 按到达时间做多重集匹配:判断「同号冲突」到底是同批数据换了主键,还是两批无关数据
|
||||
byTime := map[string][]int{}
|
||||
for pk, r := range db {
|
||||
if r.arrival != nil {
|
||||
byTime[r.arrival.UTC().Format(time.RFC3339)] = append(byTime[r.arrival.UTC().Format(time.RFC3339)], pk)
|
||||
}
|
||||
}
|
||||
matched, miss := 0, 0
|
||||
pair := [][2]int{}
|
||||
for pk, v := range bk {
|
||||
if v.Arrival == nil {
|
||||
continue
|
||||
}
|
||||
k := v.Arrival.UTC().Format(time.RFC3339)
|
||||
if list := byTime[k]; len(list) > 0 {
|
||||
matched++
|
||||
pair = append(pair, [2]int{pk, list[0]})
|
||||
byTime[k] = list[1:]
|
||||
} else {
|
||||
miss++
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n[时间对齐] 备份到访按「到达时刻」能在库内找到同刻记录: %d 条,找不到: %d 条\n", matched, miss)
|
||||
sort.Slice(pair, func(i, j int) bool { return pair[i][0] < pair[j][0] })
|
||||
fmt.Printf("[时间对齐] 同刻对的 (备份pk→库pk) 前 12 组: ")
|
||||
for i, p := range pair {
|
||||
if i >= 12 {
|
||||
break
|
||||
}
|
||||
fmt.Printf("%d→%d ", p[0], p[1])
|
||||
}
|
||||
fmt.Println()
|
||||
nDiff := 0
|
||||
for _, p := range pair {
|
||||
if p[0] != p[1] {
|
||||
nDiff++
|
||||
}
|
||||
}
|
||||
fmt.Printf("[时间对齐] 同刻但主键不同: %d / %d\n", nDiff, len(pair))
|
||||
|
||||
show := func(label string, pks []int, f func(pk int) string) {
|
||||
if len(pks) == 0 {
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n[%s] 共 %d 条,前 8 条:\n", label, len(pks))
|
||||
for i, pk := range pks {
|
||||
if i >= 8 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" #%d %s\n", pk, f(pk))
|
||||
}
|
||||
}
|
||||
show("仅备份有", onlyBackup, func(pk int) string {
|
||||
v := bk[pk]
|
||||
return fmt.Sprintf("到访 %s 离开 %s 地点src=%s", ft(v.Arrival), ft(v.Departure), ps(v.LocationSrcPK))
|
||||
})
|
||||
show("仅库里有", onlyDB, func(pk int) string {
|
||||
r := db[pk]
|
||||
return fmt.Sprintf("到访 %s 离开 %s 地点src=%d 站点新增=%v 备注=%q",
|
||||
ft(r.arrival), ftp(r.departure), r.placeSrc, r.userAdd, r.remark)
|
||||
})
|
||||
show("冲突", bothDiff, func(pk int) string {
|
||||
return vdiff(bk[pk], db[pk])
|
||||
})
|
||||
}
|
||||
|
||||
func vdiff(v rond.Visit, r visitRow) string {
|
||||
var d string
|
||||
if !eq(v.Arrival, r.arrival) {
|
||||
d += fmt.Sprintf("到达 %s→%s ", ft(v.Arrival), ft(r.arrival))
|
||||
}
|
||||
if !eq(v.Departure, r.departure) {
|
||||
d += fmt.Sprintf("离开 %s→%s ", ft(v.Departure), ft(r.departure))
|
||||
}
|
||||
if pv(v.LocationSrcPK) != r.placeSrc {
|
||||
d += fmt.Sprintf("地点 %s→%d ", ps(v.LocationSrcPK), r.placeSrc)
|
||||
}
|
||||
if av(v.ActivitySrcPK) != r.actSrc {
|
||||
d += fmt.Sprintf("活动 %s→%d ", ps(v.ActivitySrcPK), r.actSrc)
|
||||
}
|
||||
if v.Remark != r.remark {
|
||||
d += fmt.Sprintf("备注 %q→%q ", v.Remark, r.remark)
|
||||
}
|
||||
if v.Emoji != r.emoji {
|
||||
d += fmt.Sprintf("表情 %q→%q ", v.Emoji, r.emoji)
|
||||
}
|
||||
if v.Bookmarked != r.bookmarked {
|
||||
d += fmt.Sprintf("收藏 %v→%v ", v.Bookmarked, r.bookmarked)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func diffPlaces(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
fmt.Printf("\n---------- 地点 ----------\n")
|
||||
type prow struct {
|
||||
name, poi, cc, prov, city, dist, sub, thor, tz string
|
||||
lat, lon float64
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `SELECT src_pk, COALESCE(name,''), COALESCE(poi_category,''),
|
||||
COALESCE(country_code,''), COALESCE(province,''), COALESCE(city,''), COALESCE(district,''),
|
||||
COALESCE(sublocality,''), COALESCE(thoroughfare,''), COALESCE(timezone,''),
|
||||
COALESCE(lat,0), COALESCE(lon,0) FROM places WHERE dataset_id=$1`, dsID)
|
||||
must(err)
|
||||
db := map[int]prow{}
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var r prow
|
||||
must(rows.Scan(&pk, &r.name, &r.poi, &r.cc, &r.prov, &r.city, &r.dist, &r.sub, &r.thor, &r.tz, &r.lat, &r.lon))
|
||||
db[pk] = r
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
bk := map[int]rond.Location{}
|
||||
for _, l := range b.Locations {
|
||||
bk[l.SrcPK] = l
|
||||
}
|
||||
var onlyB, onlyD, diff []int
|
||||
for pk, l := range bk {
|
||||
r, ok := db[pk]
|
||||
if !ok {
|
||||
onlyB = append(onlyB, pk)
|
||||
continue
|
||||
}
|
||||
if l.Name != r.name || l.POICategory != r.poi || l.Province != r.prov || l.City != r.city ||
|
||||
l.District != r.dist || l.Sublocality != r.sub || l.Thoroughfare != r.thor || l.Timezone != r.tz ||
|
||||
l.CountryCode != r.cc || !near(l.Lat, r.lat) || !near(l.Lon, r.lon) {
|
||||
diff = append(diff, pk)
|
||||
}
|
||||
}
|
||||
for pk := range db {
|
||||
if _, ok := bk[pk]; !ok {
|
||||
onlyD = append(onlyD, pk)
|
||||
}
|
||||
}
|
||||
sort.Ints(onlyB)
|
||||
sort.Ints(onlyD)
|
||||
sort.Ints(diff)
|
||||
fmt.Printf("仅备份有: %d,仅库里有: %d,同号内容不同: %d\n", len(onlyB), len(onlyD), len(diff))
|
||||
for i, pk := range onlyB {
|
||||
if i >= 6 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" 仅备份 #%d %s (%s %s %s)\n", pk, bk[pk].Name, bk[pk].Province, bk[pk].City, bk[pk].POICategory)
|
||||
}
|
||||
for i, pk := range onlyD {
|
||||
if i >= 6 {
|
||||
break
|
||||
}
|
||||
fmt.Printf(" 仅库内 #%d %s (%s %s)\n", pk, db[pk].name, db[pk].prov, db[pk].city)
|
||||
}
|
||||
for i, pk := range diff {
|
||||
if i >= 6 {
|
||||
break
|
||||
}
|
||||
l, r := bk[pk], db[pk]
|
||||
fmt.Printf(" 冲突 #%d 备份[name=%q city=%q %.6f,%.6f] 库[name=%q city=%q %.6f,%.6f]\n",
|
||||
pk, l.Name, l.City, l.Lat, l.Lon, r.name, r.city, r.lat, r.lon)
|
||||
}
|
||||
}
|
||||
|
||||
func diffMovements(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
fmt.Printf("\n---------- 位移 ----------\n")
|
||||
rows, err := pg.QueryContext(ctx, `SELECT src_pk, COALESCE(transport_src,0),
|
||||
COALESCE(from_visit_src,0), COALESCE(to_visit_src,0), started_at, ended_at
|
||||
FROM movements WHERE dataset_id=$1`, dsID)
|
||||
must(err)
|
||||
type mrow struct {
|
||||
t, f, to int
|
||||
s, e *time.Time
|
||||
}
|
||||
db := map[int]mrow{}
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var r mrow
|
||||
var s, e sql.NullTime
|
||||
must(rows.Scan(&pk, &r.t, &r.f, &r.to, &s, &e))
|
||||
if s.Valid {
|
||||
r.s = &s.Time
|
||||
}
|
||||
if e.Valid {
|
||||
r.e = &e.Time
|
||||
}
|
||||
db[pk] = r
|
||||
}
|
||||
rows.Close()
|
||||
var onlyB, onlyD int
|
||||
for _, m := range b.Movements {
|
||||
if _, ok := db[m.SrcPK]; !ok {
|
||||
onlyB++
|
||||
}
|
||||
}
|
||||
for pk := range db {
|
||||
found := false
|
||||
for _, m := range b.Movements {
|
||||
if m.SrcPK == pk {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
onlyD++
|
||||
}
|
||||
}
|
||||
fmt.Printf("备份 %d 条 / 库内 %d 条;仅备份有 %d,仅库里有 %d\n", len(b.Movements), len(db), onlyB, onlyD)
|
||||
}
|
||||
|
||||
func diffWeather(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
var n int
|
||||
must(pg.QueryRowContext(ctx, `SELECT count(*) FROM weather WHERE dataset_id=$1`, dsID).Scan(&n))
|
||||
fmt.Printf("\n---------- 天气 ----------\n备份 %d 条 / 库内 %d 条\n", len(b.Weather), n)
|
||||
}
|
||||
|
||||
func diffActivities(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
fmt.Printf("\n---------- 活动类型 ----------\n")
|
||||
rows, err := pg.QueryContext(ctx, `SELECT src_pk, name FROM activities WHERE dataset_id=$1`, dsID)
|
||||
must(err)
|
||||
db := map[int]string{}
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var n string
|
||||
must(rows.Scan(&pk, &n))
|
||||
db[pk] = n
|
||||
}
|
||||
rows.Close()
|
||||
for _, a := range b.Activities {
|
||||
n, ok := db[a.SrcPK]
|
||||
if !ok {
|
||||
fmt.Printf(" 仅备份 #%d %q\n", a.SrcPK, a.Name)
|
||||
} else if n != a.Name {
|
||||
fmt.Printf(" 冲突 #%d 备份=%q 库=%q\n", a.SrcPK, a.Name, n)
|
||||
}
|
||||
}
|
||||
for pk, n := range db {
|
||||
found := false
|
||||
for _, a := range b.Activities {
|
||||
if a.SrcPK == pk {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fmt.Printf(" 仅库内 #%d %q\n", pk, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func diffTags(ctx context.Context, pg *sql.DB, dsID int64, b *rond.Backup) {
|
||||
var n int
|
||||
must(pg.QueryRowContext(ctx, `SELECT count(*) FROM tags WHERE dataset_id=$1`, dsID).Scan(&n))
|
||||
fmt.Printf("\n---------- 标签 ----------\n备份 %d 条 / 库内 %d 条\n", len(b.Tags), n)
|
||||
}
|
||||
|
||||
func eq(a, b *time.Time) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == nil && b == nil
|
||||
}
|
||||
return a.Equal(*b)
|
||||
}
|
||||
|
||||
func pv(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func av(p *int) int { return pv(p) }
|
||||
|
||||
func ps(p *int) string {
|
||||
if p == nil {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprint(*p)
|
||||
}
|
||||
|
||||
func ft(t *time.Time) string {
|
||||
if t == nil {
|
||||
return "-"
|
||||
}
|
||||
return t.Local().Format("01-02 15:04")
|
||||
}
|
||||
|
||||
func ftp(t *time.Time) string { return ft(t) }
|
||||
|
||||
func near(a, b float64) bool {
|
||||
d := a - b
|
||||
return d < 1e-9 && d > -1e-9
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "错误:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// 一次性探针:调用站点的导出逻辑产出一份 .rondbackup,用于核对
|
||||
// 「站点导出 → 导入 rond → rond 再导出」这条链路上 UUID 与 Z_PK 是否还连贯。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 5 {
|
||||
fmt.Println("用法: exportprobe <dsn> <datasetID> <baseline.sqlite> <out.rondbackup>")
|
||||
os.Exit(2)
|
||||
}
|
||||
dsID, err := strconv.ParseInt(os.Args[2], 10, 64)
|
||||
must(err)
|
||||
db, err := sql.Open("pgx", os.Args[1])
|
||||
must(err)
|
||||
defer db.Close()
|
||||
|
||||
f, err := os.Create(os.Args[4])
|
||||
must(err)
|
||||
defer f.Close()
|
||||
must(rond.ExportRondbackup(context.Background(), db, dsID, os.Args[3], f))
|
||||
fmt.Printf("已导出到 %s\n", os.Args[4])
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "错误:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// 一次性探针:把一份 .fwss 与 PG 里的 fog_blocks 逐块比对,量化「叠加 / 替换」两种模式的影响。
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"rond-with-you/internal/fog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("用法: fogprobe <snapshot.fwss> <dsn> [userID]")
|
||||
os.Exit(2)
|
||||
}
|
||||
uid := int64(1)
|
||||
if len(os.Args) > 3 {
|
||||
uid, _ = strconv.ParseInt(os.Args[3], 10, 64)
|
||||
}
|
||||
data, err := os.ReadFile(os.Args[1])
|
||||
must(err)
|
||||
inc, incCells, err := fog.Decode(data)
|
||||
must(err)
|
||||
fmt.Printf("快照 %s: %d 块 / %d 格\n", os.Args[1], len(inc), incCells)
|
||||
|
||||
pg, err := sql.Open("pgx", os.Args[2])
|
||||
must(err)
|
||||
defer pg.Close()
|
||||
rows, err := pg.Query(`SELECT gbx, gby, bitmap FROM fog_blocks WHERE user_id=$1`, uid)
|
||||
must(err)
|
||||
local := map[[2]int][]byte{}
|
||||
localCells := 0
|
||||
for rows.Next() {
|
||||
var gx, gy int
|
||||
var bm []byte
|
||||
must(rows.Scan(&gx, &gy, &bm))
|
||||
local[[2]int{gx, gy}] = bm
|
||||
localCells += popcount(bm)
|
||||
}
|
||||
rows.Close()
|
||||
fmt.Printf("库内: %d 块 / %d 格\n", len(local), localCells)
|
||||
|
||||
var newBlocks, newCells int
|
||||
var novBlock, overlap int
|
||||
var overlapGain int
|
||||
for _, b := range inc {
|
||||
k := [2]int{b.GX, b.GY}
|
||||
old, ok := local[k]
|
||||
if !ok {
|
||||
newBlocks++
|
||||
newCells += popcount(b.Bitmap)
|
||||
continue
|
||||
}
|
||||
novBlock++
|
||||
gain := 0
|
||||
for i := range b.Bitmap {
|
||||
gain += poptab[b.Bitmap[i]&^old[i]]
|
||||
}
|
||||
if gain > 0 {
|
||||
overlap++
|
||||
overlapGain += gain
|
||||
}
|
||||
}
|
||||
var lostBlocks, lostCells, lostFull int
|
||||
for k, bm := range local {
|
||||
found := false
|
||||
for _, b := range inc {
|
||||
if b.GX == k[0] && b.GY == k[1] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
lostBlocks++
|
||||
lostCells += popcount(bm)
|
||||
lostFull++
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n【叠加合并 overlay】\n")
|
||||
fmt.Printf(" 新增整块: %d 块(+%d 格)\n", newBlocks, newCells)
|
||||
fmt.Printf(" 与已有块重叠: %d 块,其中 %d 块含新格(+%d 格)\n", novBlock, overlap, overlapGain)
|
||||
fmt.Printf(" 合计新增格: %d,合并后总量: %d 块 / %d 格\n",
|
||||
newCells+overlapGain, len(local)+newBlocks, localCells+newCells+overlapGain)
|
||||
|
||||
fmt.Printf("\n【整体替换 cover】\n")
|
||||
fmt.Printf(" 库里现有 %d 块会被丢弃(快照里完全没有这些块),损失 %d 格\n", lostFull, lostCells)
|
||||
fmt.Printf(" 替换后总量: %d 块 / %d 格\n", len(inc), incCells)
|
||||
fmt.Printf(" 净变化: %+d 块 / %+d 格\n", len(inc)-len(local), incCells-localCells)
|
||||
|
||||
fmt.Printf("\n【其它图层】\n")
|
||||
layers, err := fog.RawLayers(data)
|
||||
if err == nil {
|
||||
for k, v := range layers {
|
||||
fmt.Printf(" %s (%d 字节, %s)\n", k, len(v), hex.EncodeToString(v[:min(8, len(v))]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var poptab = func() [256]int {
|
||||
var t [256]int
|
||||
for i := range t {
|
||||
t[i] = strings.Count(strconv.FormatUint(uint64(i), 2), "1")
|
||||
}
|
||||
return t
|
||||
}()
|
||||
|
||||
func popcount(b []byte) int {
|
||||
n := 0
|
||||
for _, v := range b {
|
||||
n += poptab[v]
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "错误:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// 一次性探针:检查 rond 备份里是否存在「跨备份稳定」的对齐键。
|
||||
// 用完即可删。
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("用法: idprobe <a.rondbackup> <b.rondbackup>")
|
||||
os.Exit(2)
|
||||
}
|
||||
a := load(os.Args[1])
|
||||
b := load(os.Args[2])
|
||||
fmt.Printf("\n=== A=%s ===\n=== B=%s ===\n", os.Args[1], os.Args[2])
|
||||
|
||||
fmt.Printf("\n---------- ZVISIT.ZIDENTIFIER_ 稳定性 ----------\n")
|
||||
fmt.Printf("A 有 UUID %d / %d,B 有 UUID %d / %d\n", len(a.visitUUID), len(a.visitPK), len(b.visitUUID), len(b.visitPK))
|
||||
inter := 0
|
||||
for u := range a.visitUUID {
|
||||
if _, ok := b.visitUUID[u]; ok {
|
||||
inter++
|
||||
}
|
||||
}
|
||||
fmt.Printf("两次备份 UUID 交集: %d(A 独有 %d,B 独有 %d)\n",
|
||||
inter, len(a.visitUUID)-inter, len(b.visitUUID)-inter)
|
||||
// UUID 相同则对比 pk 是否相同
|
||||
samePK, diffPK := 0, 0
|
||||
for u, pa := range a.visitUUID {
|
||||
if pb, ok := b.visitUUID[u]; ok {
|
||||
if pa == pb {
|
||||
samePK++
|
||||
} else {
|
||||
diffPK++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("UUID 相同者中:Z_PK 也相同 %d,Z_PK 已变 %d\n", samePK, diffPK)
|
||||
|
||||
fmt.Printf("\n---------- ZACTIVITY.ZUID_ ----------\n")
|
||||
fmt.Printf("A UUID %d / %d,B UUID %d / %d\n", len(a.actUUID), len(a.actPK), len(b.actUUID), len(b.actPK))
|
||||
same, dif := 0, 0
|
||||
for u, pa := range a.actUUID {
|
||||
if pb, ok := b.actUUID[u]; ok {
|
||||
if pa == pb {
|
||||
same++
|
||||
} else {
|
||||
dif++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("交集配对:Z_PK 相同 %d,Z_PK 已变 %d\n", same, dif)
|
||||
|
||||
fmt.Printf("\n---------- ZLOCATION 可用稳定列 ----------\n")
|
||||
fmt.Printf("A 列: %s\n", strings.Join(a.locCols, ", "))
|
||||
fmt.Printf("A 地点 %d,B 地点 %d\n", len(a.locPK), len(b.locPK))
|
||||
// 名称+粗坐标是否可作键
|
||||
type lk struct {
|
||||
name string
|
||||
lat float64
|
||||
lon float64
|
||||
}
|
||||
keyA := map[string][]int{}
|
||||
for pk, l := range a.locPK {
|
||||
k := fmt.Sprintf("%s|%.5f|%.5f", l.name, l.lat, l.lon)
|
||||
keyA[k] = append(keyA[k], pk)
|
||||
}
|
||||
hit, miss := 0, 0
|
||||
for pk, l := range b.locPK {
|
||||
k := fmt.Sprintf("%s|%.5f|%.5f", l.name, l.lat, l.lon)
|
||||
if _, ok := keyA[k]; ok {
|
||||
hit++
|
||||
} else {
|
||||
miss++
|
||||
if miss <= 5 {
|
||||
fmt.Printf(" B 独有地点 #%d %q %.6f,%.6f\n", pk, l.name, l.lat, l.lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("按「名称+坐标(1e-5)」键:B 命中 A %d,未命中 %d\n", hit, miss)
|
||||
}
|
||||
|
||||
type loc struct {
|
||||
name string
|
||||
lat, lon float64
|
||||
}
|
||||
|
||||
type snap struct {
|
||||
visitUUID map[string]int
|
||||
visitPK map[int]bool
|
||||
actUUID map[string]int
|
||||
actPK map[int]bool
|
||||
locPK map[int]loc
|
||||
locCols []string
|
||||
}
|
||||
|
||||
func load(path string) *snap {
|
||||
dir, err := os.MkdirTemp("", "idprobe")
|
||||
must(err)
|
||||
defer os.RemoveAll(dir)
|
||||
db, err := rond.Open(path, dir)
|
||||
must(err)
|
||||
defer db.Close()
|
||||
|
||||
s := &snap{visitUUID: map[string]int{}, visitPK: map[int]bool{},
|
||||
actUUID: map[string]int{}, actPK: map[int]bool{}, locPK: map[int]loc{}}
|
||||
|
||||
rows, err := db.Query(`SELECT Z_PK, hex(ZIDENTIFIER_) FROM ZVISIT`)
|
||||
must(err)
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var u sql.NullString
|
||||
must(rows.Scan(&pk, &u))
|
||||
s.visitPK[pk] = true
|
||||
if u.Valid && u.String != "" && strings.Trim(u.String, "0") != "" {
|
||||
s.visitUUID[u.String] = pk
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rows, err = db.Query(`SELECT Z_PK, hex(ZUID_), COALESCE(ZNAME_,'') FROM ZACTIVITY`)
|
||||
must(err)
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var u sql.NullString
|
||||
var n string
|
||||
must(rows.Scan(&pk, &u, &n))
|
||||
s.actPK[pk] = true
|
||||
if u.Valid && u.String != "" && strings.Trim(u.String, "0") != "" {
|
||||
s.actUUID[u.String] = pk
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rows, err = db.Query(`PRAGMA table_info(ZLOCATION)`)
|
||||
must(err)
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, typ string
|
||||
var notnull int
|
||||
var dflt sql.NullString
|
||||
var pk int
|
||||
must(rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk))
|
||||
s.locCols = append(s.locCols, name)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
rows, err = db.Query(`SELECT Z_PK, COALESCE(ZNAME_,''), COALESCE(ZLATITUDE,0), COALESCE(ZLONGITUDE,0) FROM ZLOCATION`)
|
||||
must(err)
|
||||
for rows.Next() {
|
||||
var pk int
|
||||
var l loc
|
||||
must(rows.Scan(&pk, &l.name, &l.lat, &l.lon))
|
||||
s.locPK[pk] = l
|
||||
}
|
||||
rows.Close()
|
||||
return s
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "错误:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// 一次性探针:在 rond 备份的 SQLite 上跑一条查询并打印结果(BLOB 打十六进制)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"rond-with-you/internal/rond"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println(`用法: sqlprobe <backup> "SELECT ..."`)
|
||||
os.Exit(2)
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "sqlprobe")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
db, err := rond.Open(os.Args[1], dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(os.Args[2])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
cols, _ := rows.Columns()
|
||||
fmt.Println(strings.Join(cols, " | "))
|
||||
for rows.Next() {
|
||||
vals := make([]any, len(cols))
|
||||
ptrs := make([]any, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
parts := make([]string, len(cols))
|
||||
for i, v := range vals {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
parts[i] = "-"
|
||||
case []byte:
|
||||
parts[i] = fmt.Sprintf("%x", t)
|
||||
default:
|
||||
parts[i] = fmt.Sprint(t)
|
||||
}
|
||||
}
|
||||
fmt.Println(strings.Join(parts, " | "))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user