Explorar o código

refactor(index)+dup(core): SBT 处理器基座合并与扫描小去重 (B1+B4)

- B1: btree_handler/compose_handler 约 85 行逐字重复合并为 sbtSecondary
  嵌入基座 (291→61 行); compose 索引白得 InsertBatch 批量插入路径
  (原退化逐条); 基座带编译期接口断言
- B4: fullTableScan 一行委托 fullTableScanRaw; 两处堆引用解码收编
  derefHeapSeg (compareFieldBytes 保留空引用=空内容语义, 与
  rowFieldForCompare 对齐); ORM Where 委托 addWhere
yl32940 hai 3 semanas
pai
achega
f91c6bd768

+ 3 - 156
DB/core/index/btree_handler.go

@@ -3,14 +3,13 @@ package index
 import (
 	"git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
 	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
-	"bytes"
-	"encoding/binary"
-	"sort"
 )
 
 // BTreeHandler — B+ 树二级索引 (IndexTypeBTree)
 // 叶子条目: [key(keyLen) + PKID(8B BE)]
-type BTreeHandler struct{}
+// IndexHandler 的 9 个方法与 InsertBatch 全部由嵌入的 sbtSecondary 基座提供
+// (与 ComposeChainHandler 共享同一份 SBT 委托实现)。
+type BTreeHandler struct{ sbtSecondary }
 
 func init() {
 	Compose.RegisterHandler(page.IndexTypeBTree, &BTreeHandler{})
@@ -22,142 +21,6 @@ func init() {
 	btree.SBTSplitCfg = btree.SplitConfig{SplitPercent: 50, SequentialRight: false}
 }
 
-func (h *BTreeHandler) Insert(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, rowData []byte, keyLen int) (uint32, error) {
-	entry := make([]byte, keyLen+8)
-	copy(entry[:keyLen], indexKey)
-	binary.BigEndian.PutUint64(entry[keyLen:], pkID)
-	rootPID := *entryPage
-	newRoot, err := btree.SBTInsert(rootPID, entry, handle)
-	if err != nil {
-		return 0, err
-	}
-	*entryPage = newRoot
-	return newRoot, nil
-}
-
-// InsertBatch 批量写入二级索引条目。
-//
-// 先把整批条目按复合键 (key, pkID) 排序, 再交给 SBTBulkInsert 做"顺序扫描式"
-// 灌叶 —— 排序批量对 B+ 树就是最优写入形态: 相邻条目几乎总落同一张叶, 树被
-// 自左向右扫一遍, 每张叶加载/落盘各一次; 而逐条路径是每条从根随机下降 + 整页
-// 回写。IndexKey 为 nil 的行不进本索引 (NULL 分量), 直接跳过。
-func (h *BTreeHandler) InsertBatch(handle page.HandleDb, entryPage *uint32, rows []BatchRow) error {
-	// 收集非 nil 键的条目 (定长 [key|pkID], 键长取首个非 nil)
-	n := 0
-	keyLen := 0
-	for i := range rows {
-		if rows[i].IndexKey != nil {
-			if n == 0 {
-				keyLen = len(rows[i].IndexKey)
-			}
-			n++
-		}
-	}
-	if n == 0 {
-		return nil
-	}
-	el := keyLen + 8
-	flat := make([]byte, n*el)
-	order := make([]int, 0, n)
-	idx := 0
-	for i := range rows {
-		r := &rows[i]
-		if r.IndexKey == nil {
-			continue
-		}
-		e := flat[idx*el : idx*el+el]
-		copy(e[:keyLen], r.IndexKey)
-		binary.BigEndian.PutUint64(e[keyLen:], r.PKID)
-		order = append(order, idx)
-		idx++
-	}
-	// 定长条目 [key|pkID] 的字节序 == 复合键 (key, pkID) 全序, 直接按字节排序
-	sort.Slice(order, func(a, b int) bool {
-		return bytes.Compare(flat[order[a]*el:order[a]*el+el], flat[order[b]*el:order[b]*el+el]) < 0
-	})
-	sortedFlat := make([]byte, n*el)
-	for j, oi := range order {
-		copy(sortedFlat[j*el:j*el+el], flat[oi*el:oi*el+el])
-	}
-
-	rootPID := *entryPage
-	newRoot, err := btree.SBTBulkInsert(rootPID, sortedFlat, el, handle)
-	if err != nil {
-		return err
-	}
-	if newRoot != *entryPage {
-		*entryPage = newRoot
-	}
-	return nil
-}
-
-func (h *BTreeHandler) Search(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int, resolve RowResolver) ([]byte, bool) {
-	rootPID := entryPage
-	if rootPID == 0 {
-		return nil, false
-	}
-	pkID, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
-	if !found {
-		return nil, false
-	}
-	// 二级索引不存行数据: 回表由索引自己完成 (上层无需知道主键索引是谁)
-	if resolve != nil {
-		if row, ok := resolve(pkID); ok {
-			return row, true
-		}
-		return nil, false
-	}
-	pkBuf := make([]byte, 8)
-	binary.BigEndian.PutUint64(pkBuf, pkID)
-	return pkBuf, true
-}
-
-func (h *BTreeHandler) Delete(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, keyLen int) error {
-	rootPID := *entryPage
-	if rootPID == 0 {
-		return page.ErrNotFound
-	}
-	// 二级索引允许重复 key, 用 (key, pkID) 精确定位本行的条目。
-	// pkID 未知(为 0)时退回按 key 搜索一个条目。
-	if pkID == 0 {
-		pk, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
-		if !found {
-			return page.ErrNotFound
-		}
-		pkID = pk
-	}
-	newRoot, err := btree.SBTDeleteWithPKID(rootPID, indexKey[:keyLen], pkID, handle)
-	if err != nil {
-		return err
-	}
-	*entryPage = newRoot
-	return nil
-}
-
-func (h *BTreeHandler) Update(handle page.HandleDb, entryPage *uint32, oldKey, newKey, rowData []byte, pkID uint64, keyLen int) error {
-	// 二级索引条目只存 [key + pkID], 不含行数据:
-	//   键未变 → 无事可做 (过去"先删再插"会让条目在重复 key 区段里错位, 属于多余动作)
-	//   键变了 → 按 (oldKey, pkID) 精确删旧条目, 再插新条目
-	if bytes.Equal(oldKey, newKey) {
-		return nil
-	}
-	if oldKey != nil {
-		if err := h.Delete(handle, entryPage, oldKey, pkID, len(oldKey)); err != nil && err != page.ErrNotFound {
-			return err
-		}
-	}
-	_, err := h.Insert(handle, entryPage, newKey, pkID, rowData, len(newKey))
-	return err
-}
-
-// IsRowStore 二级索引不承载行数据
-func (h *BTreeHandler) IsRowStore() bool { return false }
-
-// ResolveRow 二级索引无法按 pkID 取行, 由调用方的回表函数负责
-func (h *BTreeHandler) ResolveRow(handle page.HandleDb, entryPage uint32, pkID uint64, cache *[]DPRange) ([]byte, bool) {
-	return nil, false
-}
-
 // SearchAll 查找相同 key 下的所有 pkID
 func (h *BTreeHandler) SearchAll(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int) ([]uint64, bool) {
 	rootPID := entryPage
@@ -166,19 +29,3 @@ func (h *BTreeHandler) SearchAll(handle page.HandleDb, entryPage uint32, indexKe
 	}
 	return btree.SBTSearchAll(rootPID, indexKey[:keyLen], handle)
 }
-
-func (h *BTreeHandler) GetDataPageIDs(handle page.HandleDb, entryPage uint32) []uint32 {
-	// 二级索引没有 DataPage, 返回空
-	return nil
-}
-
-func (h *BTreeHandler) ReadDataPageRows(handle page.HandleDb, dpID uint32) [][]byte {
-	// 二级索引没有 DataPage
-	return nil
-}
-
-func (h *BTreeHandler) RangeScan(handle page.HandleDb, entryPage uint32,
-	startKey, endKey []byte, startInclusive, endInclusive bool, reverse bool,
-	callback func(row []byte) bool) error {
-	return btree.SBTRangeScan(entryPage, handle, startKey, endKey, startInclusive, endInclusive, reverse, callback)
-}

+ 7 - 94
DB/core/index/compose_handler.go

@@ -1,10 +1,6 @@
 package index
 
 import (
-	"bytes"
-	"encoding/binary"
-
-	"git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
 	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
 )
 
@@ -15,93 +11,10 @@ func init() {
 // ======================== IndexHandler: 复合二级索引处理器 ========================
 
 // ComposeChainHandler — 复合二级索引 (page.IndexTypeCompose)
-// 底层即 B+ 树二级索引 (与 BTreeHandler 共享 btree.SBT* 实现):
-// Insert/Search/Delete/RangeScan 全部委托 SBT, 条目格式 [key(keyLen) + PKID(8B BE)],
-// keyLen = 各分量按真实宽度定长拼接的总长 (分量不限 8 字节, 与 BTree 复合键一致)。
-type ComposeChainHandler struct{}
-
-func (h *ComposeChainHandler) Insert(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, rowData []byte, keyLen int) (uint32, error) {
-	entry := make([]byte, keyLen+8)
-	copy(entry[:keyLen], indexKey)
-	binary.BigEndian.PutUint64(entry[keyLen:], pkID)
-	rootPID := *entryPage
-	newRoot, err := btree.SBTInsert(rootPID, entry, handle)
-	if err != nil {
-		return 0, err
-	}
-	*entryPage = newRoot
-	return newRoot, nil
-}
-
-func (h *ComposeChainHandler) Search(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int, resolve RowResolver) ([]byte, bool) {
-	rootPID := entryPage
-	if rootPID == 0 {
-		return nil, false
-	}
-	pkID, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
-	if !found {
-		return nil, false
-	}
-	if resolve != nil {
-		if row, ok := resolve(pkID); ok {
-			return row, true
-		}
-		return nil, false
-	}
-	pkBuf := make([]byte, 8)
-	binary.BigEndian.PutUint64(pkBuf, pkID)
-	return pkBuf, true
-}
-
-func (h *ComposeChainHandler) Delete(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, keyLen int) error {
-	rootPID := *entryPage
-	if rootPID == 0 {
-		return page.ErrNotFound
-	}
-	if pkID == 0 {
-		pk, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
-		if !found {
-			return page.ErrNotFound
-		}
-		pkID = pk
-	}
-	newRoot, err := btree.SBTDeleteWithPKID(rootPID, indexKey[:keyLen], pkID, handle)
-	if err != nil {
-		return err
-	}
-	*entryPage = newRoot
-	return nil
-}
-
-func (h *ComposeChainHandler) Update(handle page.HandleDb, entryPage *uint32, oldKey, newKey, rowData []byte, pkID uint64, keyLen int) error {
-	if bytes.Equal(oldKey, newKey) {
-		return nil
-	}
-	if oldKey != nil {
-		if err := h.Delete(handle, entryPage, oldKey, pkID, len(oldKey)); err != nil && err != page.ErrNotFound {
-			return err
-		}
-	}
-	_, err := h.Insert(handle, entryPage, newKey, pkID, rowData, len(newKey))
-	return err
-}
-
-func (h *ComposeChainHandler) IsRowStore() bool { return false }
-
-func (h *ComposeChainHandler) ResolveRow(handle page.HandleDb, entryPage uint32, pkID uint64, cache *[]DPRange) ([]byte, bool) {
-	return nil, false
-}
-
-func (h *ComposeChainHandler) GetDataPageIDs(handle page.HandleDb, entryPage uint32) []uint32 {
-	return nil
-}
-
-func (h *ComposeChainHandler) ReadDataPageRows(handle page.HandleDb, dpID uint32) [][]byte {
-	return nil
-}
-
-func (h *ComposeChainHandler) RangeScan(handle page.HandleDb, entryPage uint32,
-	startKey, endKey []byte, startInclusive, endInclusive bool, reverse bool,
-	callback func(row []byte) bool) error {
-	return btree.SBTRangeScan(entryPage, handle, startKey, endKey, startInclusive, endInclusive, reverse, callback)
-}
+// 底层即 B+ 树二级索引: IndexHandler 的 9 个方法与 InsertBatch 全部由嵌入的
+// sbtSecondary 基座提供 (与 BTreeHandler 共享 btree.SBT* 实现), 条目格式
+// [key(keyLen) + PKID(8B BE)], keyLen = 各分量按真实宽度定长拼接的总长
+// (分量不限 8 字节, 与 BTree 复合键一致)。
+// InsertBatch 随基座而来: 复合索引的批量写入自动走排序批量灌叶路径,
+// 不再退化为逐条 Insert (IndexMetaPage.InsertRows 的 BatchInserter 分发)。
+type ComposeChainHandler struct{ sbtSecondary }

+ 177 - 0
DB/core/index/sbt_handler.go

@@ -0,0 +1,177 @@
+package index
+
+import (
+	"bytes"
+	"encoding/binary"
+	"sort"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
+	"git.ycl.pub/yl32940/TsDB2/DB/core/page"
+)
+
+// sbtSecondary — SBT 二级索引处理器基座: BTreeHandler / ComposeChainHandler 共享的
+// 全部 IndexHandler 方法与批量写入路径。
+//
+// 两个二级索引处理器对 IndexHandler 的实现逐字等价 (条目都是 [key(keyLen) +
+// PKID(8B BE)], 全部委托 btree.SBT*), 于是收敛到这一份嵌入实现; 各类型只保留
+// 自己的索引类型语义与独有方法 (如 BTreeHandler.SearchAll)。
+type sbtSecondary struct{}
+
+// 嵌入基座后两个处理器的方法集在编译期锁定: IndexHandler 全数命中,
+// BatchInserter 随之命中 —— IndexMetaPage.InsertRows 的批量分发对复合索引同样生效。
+var (
+	_ IndexHandler  = (*sbtSecondary)(nil)
+	_ BatchInserter = (*sbtSecondary)(nil)
+)
+
+func (h *sbtSecondary) Insert(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, rowData []byte, keyLen int) (uint32, error) {
+	entry := make([]byte, keyLen+8)
+	copy(entry[:keyLen], indexKey)
+	binary.BigEndian.PutUint64(entry[keyLen:], pkID)
+	rootPID := *entryPage
+	newRoot, err := btree.SBTInsert(rootPID, entry, handle)
+	if err != nil {
+		return 0, err
+	}
+	*entryPage = newRoot
+	return newRoot, nil
+}
+
+// InsertBatch 批量写入二级索引条目。
+//
+// 先把整批条目按复合键 (key, pkID) 排序, 再交给 SBTBulkInsert 做"顺序扫描式"
+// 灌叶 —— 排序批量对 B+ 树就是最优写入形态: 相邻条目几乎总落同一张叶, 树被
+// 自左向右扫一遍, 每张叶加载/落盘各一次; 而逐条路径是每条从根随机下降 + 整页
+// 回写。IndexKey 为 nil 的行不进本索引 (NULL 分量), 直接跳过。
+func (h *sbtSecondary) InsertBatch(handle page.HandleDb, entryPage *uint32, rows []BatchRow) error {
+	// 收集非 nil 键的条目 (定长 [key|pkID], 键长取首个非 nil)
+	n := 0
+	keyLen := 0
+	for i := range rows {
+		if rows[i].IndexKey != nil {
+			if n == 0 {
+				keyLen = len(rows[i].IndexKey)
+			}
+			n++
+		}
+	}
+	if n == 0 {
+		return nil
+	}
+	el := keyLen + 8
+	flat := make([]byte, n*el)
+	order := make([]int, 0, n)
+	idx := 0
+	for i := range rows {
+		r := &rows[i]
+		if r.IndexKey == nil {
+			continue
+		}
+		e := flat[idx*el : idx*el+el]
+		copy(e[:keyLen], r.IndexKey)
+		binary.BigEndian.PutUint64(e[keyLen:], r.PKID)
+		order = append(order, idx)
+		idx++
+	}
+	// 定长条目 [key|pkID] 的字节序 == 复合键 (key, pkID) 全序, 直接按字节排序
+	sort.Slice(order, func(a, b int) bool {
+		return bytes.Compare(flat[order[a]*el:order[a]*el+el], flat[order[b]*el:order[b]*el+el]) < 0
+	})
+	sortedFlat := make([]byte, n*el)
+	for j, oi := range order {
+		copy(sortedFlat[j*el:j*el+el], flat[oi*el:oi*el+el])
+	}
+
+	rootPID := *entryPage
+	newRoot, err := btree.SBTBulkInsert(rootPID, sortedFlat, el, handle)
+	if err != nil {
+		return err
+	}
+	if newRoot != *entryPage {
+		*entryPage = newRoot
+	}
+	return nil
+}
+
+func (h *sbtSecondary) Search(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int, resolve RowResolver) ([]byte, bool) {
+	rootPID := entryPage
+	if rootPID == 0 {
+		return nil, false
+	}
+	pkID, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
+	if !found {
+		return nil, false
+	}
+	// 二级索引不存行数据: 回表由索引自己完成 (上层无需知道主键索引是谁)
+	if resolve != nil {
+		if row, ok := resolve(pkID); ok {
+			return row, true
+		}
+		return nil, false
+	}
+	pkBuf := make([]byte, 8)
+	binary.BigEndian.PutUint64(pkBuf, pkID)
+	return pkBuf, true
+}
+
+func (h *sbtSecondary) Delete(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, keyLen int) error {
+	rootPID := *entryPage
+	if rootPID == 0 {
+		return page.ErrNotFound
+	}
+	// 二级索引允许重复 key, 用 (key, pkID) 精确定位本行的条目。
+	// pkID 未知(为 0)时退回按 key 搜索一个条目。
+	if pkID == 0 {
+		pk, found := btree.SBTSearch(rootPID, indexKey[:keyLen], handle)
+		if !found {
+			return page.ErrNotFound
+		}
+		pkID = pk
+	}
+	newRoot, err := btree.SBTDeleteWithPKID(rootPID, indexKey[:keyLen], pkID, handle)
+	if err != nil {
+		return err
+	}
+	*entryPage = newRoot
+	return nil
+}
+
+func (h *sbtSecondary) Update(handle page.HandleDb, entryPage *uint32, oldKey, newKey, rowData []byte, pkID uint64, keyLen int) error {
+	// 二级索引条目只存 [key + pkID], 不含行数据:
+	//   键未变 → 无事可做 (过去"先删再插"会让条目在重复 key 区段里错位, 属于多余动作)
+	//   键变了 → 按 (oldKey, pkID) 精确删旧条目, 再插新条目
+	if bytes.Equal(oldKey, newKey) {
+		return nil
+	}
+	if oldKey != nil {
+		if err := h.Delete(handle, entryPage, oldKey, pkID, len(oldKey)); err != nil && err != page.ErrNotFound {
+			return err
+		}
+	}
+	_, err := h.Insert(handle, entryPage, newKey, pkID, rowData, len(newKey))
+	return err
+}
+
+// IsRowStore 二级索引不承载行数据
+func (h *sbtSecondary) IsRowStore() bool { return false }
+
+// ResolveRow 二级索引无法按 pkID 取行, 由调用方的回表函数负责
+func (h *sbtSecondary) ResolveRow(handle page.HandleDb, entryPage uint32, pkID uint64, cache *[]DPRange) ([]byte, bool) {
+	return nil, false
+}
+
+func (h *sbtSecondary) GetDataPageIDs(handle page.HandleDb, entryPage uint32) []uint32 {
+	// 二级索引没有 DataPage, 返回空
+	return nil
+}
+
+func (h *sbtSecondary) ReadDataPageRows(handle page.HandleDb, dpID uint32) [][]byte {
+	// 二级索引没有 DataPage
+	return nil
+}
+
+func (h *sbtSecondary) RangeScan(handle page.HandleDb, entryPage uint32,
+	startKey, endKey []byte, startInclusive, endInclusive bool, reverse bool,
+	callback func(row []byte) bool) error {
+	return btree.SBTRangeScan(entryPage, handle, startKey, endKey, startInclusive, endInclusive, reverse, callback)
+}

+ 2 - 2
DB/core/index_meta_page.go

@@ -679,8 +679,8 @@ func (im *IndexMetaPage) InsertRow(fields []types.FieldInfo, pkID uint64, rowDat
 //   - 唯一约束仍是"先全部预检、再动任何写入"—— 每行对既有树的查重照做,
 //     另补"批内查重"(单行路径里批内冲突会在第 i+1 行的树查重中被发现, 批量
 //     路径一次性排序比出来, 语义等价且更便宜)。
-//   - 实现了 BatchInserter 的索引 (主键 B+ 树 / 二级 B+ 树) 走批量路径;
-//     其余 (时序 / hash / 复合链) 退化为逐条 Insert, 行为与单行路径逐字节一致。
+//   - 实现了 BatchInserter 的索引 (主键 B+ 树 / 二级 B+ 树 / 复合索引) 走批量路径;
+//     其余 (时序 / hash) 退化为逐条 Insert, 行为与单行路径逐字节一致。
 //
 // 失败语义与单行路径相同: 中途报错时前面已写入的索引不回滚 (调用方靠事务回滚)。
 func (im *IndexMetaPage) InsertRows(fields []types.FieldInfo, pkIDs []uint64, rowDatas [][]byte, activeVer byte, versions []SchemaVersion) error {

+ 6 - 26
DB/core/query_exec.go

@@ -127,33 +127,12 @@ func (q *Query) indexScan(plan *scanPlan, callback func(pkID uint64, row []types
 	return nil
 }
 
-// fullTableScan 全表扫描 (流式回调模式)
+// fullTableScan 全表扫描 (流式回调模式)。与 fullTableScanRaw 同构,
+// 回调前多一步 deserialize 整行。
 func (q *Query) fullTableScan(callback func(pkID uint64, row []types.FieldValue) bool) error {
-	// 找承载行数据的索引 (由处理器自报, 本层不认识索引类型)
-	primMeta, primHandler, ok := q.table.indexMeta.rowStoreMeta()
-	if !ok || primMeta == nil || primHandler == nil {
-		return nil
-	}
-
-	dpIDs := primHandler.GetDataPageIDs(q.table.handle, primMeta.EntryPage)
-	for _, dpID := range dpIDs {
-		rows := primHandler.ReadDataPageRows(q.table.handle, dpID)
-		for _, storeRow := range rows {
-			if len(storeRow) < 8 {
-				continue
-			}
-			rowData := storeRow[8:]
-			if q.whereCond != nil {
-				if !q.whereCond.Match(rowData, q.table) {
-					continue
-				}
-			}
-			if !callback(binary.BigEndian.Uint64(storeRow[:8]), q.deserialize(rowData)) {
-				return nil // 回调要求提前终止
-			}
-		}
-	}
-	return nil
+	return q.fullTableScanRaw(func(pkID uint64, rowData []byte) bool {
+		return callback(pkID, q.deserialize(rowData))
+	})
 }
 
 // ======================== 裸字节扫描 (聚合专用) ========================
@@ -245,6 +224,7 @@ func (q *Query) indexScanRaw(plan *scanPlan, callback func(pkID uint64, rowData
 // fullTableScanRaw 与 fullTableScan 同构, 回调拿裸行字节。
 // ReadDataPageRows 返回的行本就是拷贝快照, rowData 跨回调安全。
 func (q *Query) fullTableScanRaw(callback func(pkID uint64, rowData []byte) bool) error {
+	// 找承载行数据的索引 (由处理器自报, 本层不认识索引类型)
 	primMeta, primHandler, ok := q.table.indexMeta.rowStoreMeta()
 	if !ok || primMeta == nil || primHandler == nil {
 		return nil

+ 6 - 5
DB/core/table_compare.go

@@ -118,14 +118,15 @@ func (table *Table) derefHeapSeg(seg []byte) ([]byte, bool) {
 
 func (table *Table) compareFieldBytes(seg []byte, ct colType, value interface{}) (int, bool) {
 	if types.IsDynamic(ct.typ) {
+		// derefHeapSeg 把 pageID==0 与读失败统一报成 ok=false; 本处的契约与 heapRead
+		// 同构, 二者必须区分: 短引用 / 读失败 → 不可比; pageID==0 (空引用) 是空内容,
+		// 照常参与下面的比较 (与 rowFieldForCompare / LikeCondition 的空引用语义一致)。
 		if len(seg) < types.DynamicRefSize {
 			return 0, false
 		}
-		pageID := binary.BigEndian.Uint32(seg[:4])
-		offInPage := binary.BigEndian.Uint16(seg[4:6])
-		stored, err := table.heapRead(pageID, offInPage)
-		if err != nil {
-			return 0, false
+		stored, ok := table.derefHeapSeg(seg)
+		if !ok && binary.BigEndian.Uint32(seg[:4]) != 0 {
+			return 0, false // 真读失败 → 不可比
 		}
 		switch v := value.(type) {
 		case []byte:

+ 2 - 21
DB/core/table_rowcodec.go

@@ -697,15 +697,7 @@ func (table *Table) deserializeRowMode(rowData []byte, want []bool, keepNull boo
 		switch {
 		case types.IsDynamic(f.FieldType) || types.RowFieldRefTagged(rowData, i, col0Width):
 			// 解引用堆页。NULL / 未提供的动态列内容为 0 字节或全零引用 —— 读不到就是没有值。
-			if len(seg) < types.DynamicRefSize {
-				continue
-			}
-			pageID := binary.BigEndian.Uint32(seg[:4])
-			offInPage := binary.BigEndian.Uint16(seg[4:6])
-			if pageID == 0 {
-				continue // 空引用 = 空内容, 不产出"空值"
-			}
-			if data, err := table.heapRead(pageID, offInPage); err == nil {
+			if data, ok := table.derefHeapSeg(seg); ok {
 				val = data
 			}
 		case types.IsBlob(f.FieldType):
@@ -793,18 +785,7 @@ func (table *Table) deserializeField(rowData []byte, fieldName string) (interfac
 	seg := rowData[start:end]
 	switch {
 	case types.IsDynamic(f.FieldType) || types.RowFieldRefTagged(rowData, pos, col0Width):
-		if len(seg) < types.DynamicRefSize {
-			return nil, false
-		}
-		pageID := binary.BigEndian.Uint32(seg[:4])
-		offInPage := binary.BigEndian.Uint16(seg[4:6])
-		if pageID == 0 {
-			return nil, false
-		}
-		if data, err := table.heapRead(pageID, offInPage); err == nil {
-			return data, true
-		}
-		return nil, false
+		return table.derefHeapSeg(seg)
 	case types.IsBlob(f.FieldType):
 		if len(seg) < types.BlobRefSize {
 			return nil, false

+ 1 - 2
DB/query.go

@@ -35,8 +35,7 @@ func (q *Query[T]) addWhere(field, op string, value interface{}) *Query[T] {
 
 // Where 添加条件
 func (q *Query[T]) Where(field string, op string, value interface{}) *Query[T] {
-	q.wheres = append(q.wheres, whereClause{field: field, op: op, value: value})
-	return q
+	return q.addWhere(field, op, value)
 }
 
 // OrderBy 排序