| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- package core
- // 本文件验证【值级溢出】: 行内容超 MaxInlineRowData 时, 未建索引的宽字符串列落到堆页,
- // 行内只留 6B 堆引用 —— "一页两行"的政策由此从"建表时按声明宽度卡死"变成"写入时按实际
- // 行宽动态保住"。要钉住的性质:
- //
- // 1. 写入: 超限的行不再整行拒绝, 而是真的被缩短到 MaxInlineRowData 以内 (行内出现引用)。
- // 2. 读回: 溢出的列解引用后逐字节一致; 同行未溢出的列不受影响。
- // 3. 逐行判定: 同一列的【不同行】可以一行内联、一行溢出, 过滤必须按行内 tag 走对分支
- // —— 这是最容易错的一处: 只看列类型就会把 6B 页号当成字符串内容去比, 症状是静默漏行。
- // 4. 更新: 溢出行改写后仍可读回 (旧堆块复用/释放)。
- // 5. 删除: 行与堆块一并消失。
- import (
- "fmt"
- "strings"
- "testing"
- tsindex "git.ycl.pub/yl32940/TsDB2/DB/core/index"
- "git.ycl.pub/yl32940/TsDB2/DB/core/page"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- )
- func spillColName(i int) string { return fmt.Sprintf("S%d", i) }
- // spillNewTable 建一张"宽字符串列都可溢出"的表: 1 个 8B 主键列 + n 个 2048 宽字符串列。
- // 列 0 是定长主键 (恒内联, 偏移表基址依赖它), 其余字符串列都没建索引 ⇒ 都可溢出。
- func spillNewTable(t *testing.T, db *DB, name string, n int) *Table {
- t.Helper()
- fields := []types.FieldInfo{{FieldType: types.FieldType_Uint64, FieldName: []byte("id")}}
- for i := 0; i < n; i++ {
- fields = append(fields, types.FieldInfo{
- FieldType: types.FieldType_String,
- Flag: types.WidthCodeFor(types.MaxInlineColumnWidth),
- FieldName: []byte(spillColName(i)),
- })
- }
- tbl, err := db.CreateTable(name, fields)
- if err != nil {
- t.Fatalf("建表 %s: %v", name, err)
- }
- if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
- t.Fatalf("建主键索引: %v", err)
- }
- return tbl
- }
- // spillRawRow 直接按 pkID 取回【盘上原始 rowData】(去掉 8B pkID 前缀)。精确到字节, 用来断言
- // "行真的被缩短了", 而不是只看插入成不成功 —— 溢出若没生效, 行会变大但仍然插得进去。
- func spillRawRow(t *testing.T, tbl *Table, pk uint64) []byte {
- t.Helper()
- meta, handler, ok := tbl.IndexMeta().rowStoreMeta()
- if !ok {
- t.Fatal("表上没有行存储索引")
- }
- ph, ok := handler.(*tsindex.PrimaryHandler)
- if !ok {
- t.Fatalf("行存储 handler 不是 PrimaryHandler: %T", handler)
- }
- store, found := ph.SearchByPK(tbl.handle, meta.EntryPage, pk)
- if !found {
- t.Fatalf("pk=%d 读不到行", pk)
- }
- return store[8:]
- }
- func spillValues(pk uint64, vals []string) []types.FieldValue {
- fs := make([]types.FieldValue, 0, 1+len(vals))
- fs = append(fs, types.FieldValue{FieldName: []byte("id"), Value: pk})
- for i, v := range vals {
- fs = append(fs, types.FieldValue{FieldName: []byte(spillColName(i)), Value: v})
- }
- return fs
- }
- func TestValueSpillRoundTrip(t *testing.T) {
- db := fixOpenDB(t)
- const n = 5
- tbl := spillNewTable(t, db, "SPILL", n)
- // 每列 1800 字节 × 5 = 9000 > MaxInlineRowData(8159): 不溢出就装不下。
- big := make([]string, n)
- for i := range big {
- big[i] = strings.Repeat(string(rune('a'+i)), 1800)
- }
- pkBig := tbl.AllocPKID()
- if _, err := tbl.InsertRowWithID(pkBig, spillValues(pkBig, big)); err != nil {
- t.Fatalf("插入超限行: %v", err)
- }
- // 同一批列的第二行故意【不溢出】: 覆盖"同列不同行、内联与溢出并存"的路径。
- small := make([]string, n)
- for i := range small {
- small[i] = strings.Repeat(string(rune('A'+i)), 40)
- }
- pkSmall := tbl.AllocPKID()
- if _, err := tbl.InsertRowWithID(pkSmall, spillValues(pkSmall, small)); err != nil {
- t.Fatalf("插入小行: %v", err)
- }
- // 1. 大行真的被缩短到行内上限以内 (未溢出时长度 ≈ 9022)。
- raw := spillRawRow(t, tbl, pkBig)
- if len(raw) > types.MaxInlineRowData {
- t.Fatalf("行内长度 %d 超过上限 %d: 值级溢出没有生效",
- len(raw), types.MaxInlineRowData)
- }
- // 小行不该被无谓地溢出 (它本来就在限内)。
- if rawSmall := spillRawRow(t, tbl, pkSmall); len(rawSmall) > types.MaxInlineRowData {
- t.Fatalf("小行长度 %d 超过上限: 不该溢出却溢出了", len(rawSmall))
- }
- // 2. 两行都读回, 每列逐字节一致 (溢出列解引用 + 内联列并存的错位探针)。
- rows, err := tbl.Query().Find()
- if err != nil {
- t.Fatalf("find: %v", err)
- }
- if len(rows) != 2 {
- t.Fatalf("应读出 2 行, 得到 %d", len(rows))
- }
- byID := map[uint64][]types.FieldValue{}
- for _, r := range rows {
- id, ok := newTypeRowVal(t, r, "id").(uint64)
- if !ok {
- t.Fatalf("id 列不是 uint64: %T", newTypeRowVal(t, r, "id"))
- }
- byID[id] = r
- }
- for _, tc := range []struct {
- pk uint64
- want []string
- }{{pkBig, big}, {pkSmall, small}} {
- r, ok := byID[tc.pk]
- if !ok {
- t.Fatalf("pk=%d 没读回来", tc.pk)
- }
- for i, want := range tc.want {
- if got := stringTrim(newTypeRowVal(t, r, spillColName(i))); got != want {
- t.Errorf("pk=%d 列 %s 读回长度 %d, 期望 %d (溢出列解引用不一致)",
- tc.pk, spillColName(i), len(got), len(want))
- }
- }
- }
- // 3. 过滤: 对【未建索引】的列做等值过滤, 走的是行级过滤 (rowFieldForCompare) ——
- // 大行该列已溢出 (行内是 6B 引用), 小行该列内联, 两条路径都要给出正确答案。
- const probeCol = 2
- if got, err := tbl.Query().Where(Eq(spillColName(probeCol), big[probeCol], types.FieldType(0))).Count(); err != nil {
- t.Fatalf("按溢出列过滤: %v", err)
- } else if got != 1 {
- t.Errorf("溢出列等值过滤命中 %d 行, 期望 1 (把 6B 堆引用当成内容比了?)", got)
- }
- if got, err := tbl.Query().Where(Eq(spillColName(probeCol), small[probeCol], types.FieldType(0))).Count(); err != nil {
- t.Fatalf("按内联列过滤: %v", err)
- } else if got != 1 {
- t.Errorf("内联列等值过滤命中 %d 行, 期望 1", got)
- }
- // 前缀不该命中 (与内联时的语义一致: 按列宽补零比较)。
- if got, _ := tbl.Query().Where(Eq(spillColName(probeCol), big[probeCol][:8], types.FieldType(0))).Count(); got != 0 {
- t.Errorf("前缀命中 %d 行, 期望 0", got)
- }
- // LIKE 走的是另一条行级路径 (LikeCondition.Match), 同样要能解引用。
- if got, err := tbl.Query().Where(Like(spillColName(probeCol), big[probeCol][:8]+"%")).Count(); err != nil {
- t.Fatalf("LIKE: %v", err)
- } else if got != 1 {
- t.Errorf("LIKE 命中 %d 行, 期望 1", got)
- }
- // 4. 更新溢出行 (改短) 后仍可读回。
- newVals := make([]string, n)
- for i := range newVals {
- newVals[i] = strings.Repeat(string(rune('0'+i)), 1500)
- }
- if err := tbl.UpdateRowByPK(pkBig, spillValues(pkBig, newVals)); err != nil {
- t.Fatalf("更新溢出行: %v", err)
- }
- rows, err = tbl.Query().Find()
- if err != nil {
- t.Fatalf("find after update: %v", err)
- }
- for _, r := range rows {
- if id, _ := newTypeRowVal(t, r, "id").(uint64); id != pkBig {
- continue
- }
- for i, want := range newVals {
- if got := stringTrim(newTypeRowVal(t, r, spillColName(i))); got != want {
- t.Errorf("更新后列 %s 长度 %d, 期望 %d", spillColName(i), len(got), len(want))
- }
- }
- }
- // 5. 删除: 行与堆块一并消失。
- if err := tbl.DeleteRowByPK(pkBig); err != nil {
- t.Fatalf("删除: %v", err)
- }
- if got, _ := tbl.Query().Count(); got != 1 {
- t.Errorf("删除后应剩 1 行, 得到 %d", got)
- }
- }
|