Explorar el Código

fix(core): VACUUM 搬运 Blob 链内容 — 修复重建后读取 BLOB 列 EOF/损坏

DockerManager 上游报告+二分实锤: 同一源数据 migrate 后直读正常, 过一遍
VACUUM 后 param>16360 的行 (Blob 链式列) 读取必崩。机制: vacuumScan 用
deserializeRowKeepNull 拍行快照, Blob 列产出的是 8B BlobRef — 指向【旧
文件】的链页; 行搬进新库后链页并不存在, 引用悬空 (读取报 corrupted/
EOF, 视垃圾内容而定)。Blob 链页面不在行重写路径的搬运范围里。

修复: 扫描阶段对 Blob 列物化 — tbl.BlobRead(ref, CompressNone 写侧约定)
把链内容读成字节填回 vals, 新库序列化走 blobFieldRef 落【新链】。物化
失败让整个 VACUUM 报错返回 (不静默跳行丢数据)。红→绿: 修复前
'blob corrupted: decoded length mismatches TotalLen', 修复后 60000B
逐字节一致。新增 TestVacuumBlobChain 钉住。
yl32940 hace 3 semanas
padre
commit
554eab6e94
Se han modificado 2 ficheros con 95 adiciones y 0 borrados
  1. 34 0
      DB/core/vacuum.go
  2. 61 0
      DB/core/vacuum_blob_test.go

+ 34 - 0
DB/core/vacuum.go

@@ -377,13 +377,44 @@ func (db *DB) vacuumScan(progress VACUUMProgress) ([]vacuumSrcTable, int64, erro
 
 		// 行快照 (保留 NULL: deserializeRow 会跳过 NULL 列, VACUUM 会把
 		// NULL 物化成类型零值 —— 语义漂移且不可逆)。
+		// ⭐ Blob 列特判: deserialize 产出的是 8B BlobRef (指向【本文件】的链页),
+		// 直接搬过去新库后链页并不存在 —— 读回 EOF/空 (DockerManager 实测:
+		// 迁移+VACUUM 后 param 列全崩)。必须在扫描时把链内容物化进 vals,
+		// 让新库的序列化走 blobFieldRef 落【新链】。
+		blobCols := map[string]bool{}
+		for _, f := range fields {
+			if types.IsBlob(f.FieldType) {
+				blobCols[string(f.FieldName)] = true
+			}
+		}
 		rows := make([]vacuumSrcRow, 0, tbl.TotalRows())
 		q := tbl.Query()
+		var blobErr error // 闭包内物化 Blob 的失败载体 (闭包不能引用后声明的 scanErr)
 		scanErr := q.fullTableScanRaw(func(pkID uint64, rowData []byte) bool {
 			vals := tbl.deserializeRowKeepNull(rowData)
 			if vals == nil {
 				return true // 解析失败, 跳过
 			}
+			if len(blobCols) > 0 {
+				for i, fv := range vals {
+					if !blobCols[string(fv.FieldName)] {
+						continue
+					}
+					ref, ok := fv.Value.(types.BlobRef)
+					if !ok || ref.Empty() {
+						continue
+					}
+					data, err := tbl.BlobRead(ref, types.CompressNone) // 写侧约定 CompressNone
+					if err != nil {
+						blobErr = fmt.Errorf("物化 Blob 列 %q (pkID=%d): %w", fv.FieldName, pkID, err)
+						return false
+					}
+					vals[i].Value = data
+				}
+				if blobErr != nil {
+					return false
+				}
+			}
 			rows = append(rows, vacuumSrcRow{pkID: pkID, vals: vals})
 			scanned++
 			if scanned%10000 == 0 {
@@ -391,6 +422,9 @@ func (db *DB) vacuumScan(progress VACUUMProgress) ([]vacuumSrcTable, int64, erro
 			}
 			return true
 		})
+		if blobErr != nil {
+			return nil, 0, blobErr
+		}
 		if scanErr != nil {
 			return nil, 0, fmt.Errorf("扫描表 %q: %w", meta.Name, scanErr)
 		}

+ 61 - 0
DB/core/vacuum_blob_test.go

@@ -0,0 +1,61 @@
+package core
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/types"
+)
+
+func TestVacuumBlobChain(t *testing.T) {
+	db := fixOpenDB(t)
+	tbl, err := db.CreateTable("docs", []types.FieldInfo{
+		{FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
+		{FieldType: types.FieldType_Blob, FieldName: []byte("body")},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
+		t.Fatal(err)
+	}
+	big := strings.Repeat("V", 60000)
+	if _, err := tbl.InsertRowWithID(1, []types.FieldValue{
+		{FieldName: []byte("id"), Value: uint64(1)},
+		{FieldName: []byte("body"), Value: []byte(big)},
+	}); err != nil {
+		t.Fatal(err)
+	}
+	db.Commit()
+	if _, err := db.VACUUM(); err != nil {
+		t.Fatal(err)
+	}
+	tbl2, ok := db.FindTable("docs")
+	if !ok {
+		t.Fatal("vacuum 后表丢失")
+	}
+	vals, err := tbl2.FindByPK(1)
+	if err != nil {
+		t.Fatalf("VACUUM 后读取: %v", err)
+	}
+	var ref types.BlobRef
+	found := false
+	for _, fv := range vals {
+		if string(fv.FieldName) == "body" {
+			ref, found = fv.Value.(types.BlobRef)
+		}
+	}
+	if !found || ref.Empty() {
+		t.Fatalf("body 列丢失或引用为空 (found=%v ref=%+v)", found, ref)
+	}
+	body, err := tbl2.BlobRead(ref, types.CompressNone)
+	if err != nil {
+		t.Fatalf("BlobRead (驱动物化同路径): %v", err)
+	}
+	if !bytes.Equal(body, []byte(big)) {
+		t.Fatalf("VACUUM 后 body 不一致: got %d B want %d B", len(body), len(big))
+	}
+	t.Logf("VACUUM 后 blob 链完整可读 (%d B)", len(body))
+}