导出:把 ZVISIT.ZRAW 夹到包内采样行数以内(越界会让真机整包拒收)

ZVISIT.ZRAW 是「这次到访由多少个原始采样点归并而来」的缓存计数,而采样点表 ZRAWVISIT 是
**不托管实体**(只从基线原样带过来)。合并进来的到访带着源包的计数、源包的那些采样行站点
没留下 ⇒ 计数会超过包里实际有的采样行数。

2026-09-30 逐行二分定位:生产导出被真机拒收(「备份恢复失败,你的当前数据未被修改」),
罪魁是单条 `ZVISIT #1000`——它声称 2305 个采样点,而包里只有 2246 行 ZRAWVISIT;
把这一列改成 1421 后同一份包立刻能导入。旁证:被真机接受过的三份包(09-14 基线、
09-20 A 包、验收纯净往返包)的 ZRAW 最大值都恰好 ≤ 采样行数,而它们 ZRAW 为 NULL/0 的行
本来就有 39~139 条 ⇒ 往下夹是安全的、夹取只降不升。

顺带:`readDupBaseline` 先 os.Stat 再开库——sql.Open 默认 READWRITE|CREATE,路径不存在时
会静默建一个空库,报出来的是「no such table」而不是「没有基线」,很难查。

用例 `TestExportClampsRawSampleCount`:造一条声称 999999 个采样点的到访,导出后断言
「任何一行的 ZRAW 都不超过包内采样行数」且该行仍在(只改计数、不丢记录);变异验证:
关掉夹取立刻红。
This commit is contained in:
tamakyi
2026-09-30 17:35:59 +08:00
parent 9e112d55f9
commit d99c63fa4f
3 changed files with 84 additions and 0 deletions
+11
View File
@@ -511,6 +511,17 @@ func patchSQLite(ctx context.Context, db *sql.DB, datasetID int64, sqlitePath st
return fmt.Errorf("清理失去目标的原始采样点失败: %w", err)
}
// ZVISIT.ZRAW 是「这次到访由多少个原始采样点归并而来」的计数,而采样点表 ZRAWVISIT 是
// **不托管实体**(原样从基线带过来)。合并进来的到访带着源包的计数,可源包里的那些采样行
// 站点没留下 ⇒ 计数会超过包里实际有的采样行数。2026-09-30 实测:真机会因此**整包拒收**
// (「备份恢复失败,你的当前数据未被修改」)——#1000 声称 2305 个采样点而包里只有 2246 行,
// 把它夹到 2246 以内立刻就能导入。被真机接受过的包里 ZRAW 为 NULL/0 的行本来就有 39~139 条,
// 所以往下夹是安全的(夹取只降不升)。
if _, err := tx.ExecContext(ctx, `UPDATE ZVISIT SET ZRAW = (SELECT count(*) FROM ZRAWVISIT)
WHERE ZRAW IS NOT NULL AND ZRAW > (SELECT count(*) FROM ZRAWVISIT)`); err != nil {
return fmt.Errorf("夹原始采样点数失败: %w", err)
}
// 8) 访问↔标签关联(列名从基线探测得到;纯两列关系,全量重写即可)
if join.Name != "" && join.VisitCol != "" && join.TagCol != "" {
if _, err := tx.ExecContext(ctx, "DELETE FROM "+join.Name); err != nil {