stringwidth_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. package core
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "testing"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  10. )
  11. // 本文件是"字符串列宽度由列级参数决定"这一改动的端到端验证。
  12. //
  13. // 要钉住的性质只有一条, 但它极其容易静默失效: **行内字节与索引键字节同源**。
  14. // 宽度算错的后果不是报错, 而是该列之后的每一个字段都从错位偏移解码 —— 行照样能读,
  15. // 字段也都有值, 只是全是别的字段的内容。所以这里除了校验被加宽的列本身, 还特意放了
  16. // 两个"错位探针" (After / Idx): 只有它们的值也对, 才说明布局是对的。
  17. // stringTrim 定长字符串列解码出来是"补满列宽"的字节, 比较前去掉补零。
  18. func stringTrim(v interface{}) string {
  19. b, ok := v.([]byte)
  20. if !ok {
  21. return fmt.Sprintf("<非字节载体 %T>", v)
  22. }
  23. return string(bytes.TrimRight(b, "\x00"))
  24. }
  25. // TestStringColumnWidthEndToEnd 宽度码列: 写入/读取/索引/超宽报错。
  26. func TestStringColumnWidthEndToEnd(t *testing.T) {
  27. db := fixOpenDB(t)
  28. code := types.WidthCodeFor(1024) // 声明 1024 字节
  29. width := int(types.FieldSize(types.FieldType_String, code))
  30. if width < 1024 {
  31. t.Fatalf("宽度码 %d 只给出 %d 字节, 装不下声明的 1024", code, width)
  32. }
  33. tbl, err := db.CreateTable("WSTR", []types.FieldInfo{
  34. {FieldType: types.FieldType_Uint64, FieldName: []byte("ID")},
  35. {FieldType: types.FieldType_String, Flag: code, FieldName: []byte("Wide")},
  36. // Wide2 与 Wide 同宽但【不建索引】: 用来比对"索引路径"与"全表过滤路径"
  37. {FieldType: types.FieldType_String, Flag: code, FieldName: []byte("Wide2")},
  38. // After / Idx 是错位探针: Wide 的宽度算错时先烂的是它们
  39. {FieldType: types.FieldType_String, FieldName: []byte("After")},
  40. {FieldType: types.FieldType_Uint64, FieldName: []byte("Idx")},
  41. })
  42. if err != nil {
  43. t.Fatalf("create table: %v", err)
  44. }
  45. // 主键索引 = 承载行数据的行存储: 没有它 InsertRow 无处落行 (静默丢行), 全表扫也读不到
  46. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  47. t.Fatalf("create pk: %v", err)
  48. }
  49. if _, err := tbl.CreateIndex("idx_wide", page.IndexTypeBTree, []byte{1}); err != nil {
  50. t.Fatalf("在宽度码列上建索引: %v", err)
  51. }
  52. // 1 字节 / 500 字节 / 正好填满列宽: 最后一档覆盖"边界处不允许溢出"
  53. vals := []string{"a", strings.Repeat("b", 500), strings.Repeat("c", width)}
  54. after := func(i int) string { return fmt.Sprintf("%012d", 1000+i) }
  55. for i, v := range vals {
  56. if _, err := tbl.InsertRow([]types.FieldValue{
  57. {FieldName: []byte("ID"), Value: uint64(i + 1)},
  58. {FieldName: []byte("Wide"), Value: v},
  59. {FieldName: []byte("Wide2"), Value: v},
  60. {FieldName: []byte("After"), Value: after(i)},
  61. {FieldName: []byte("Idx"), Value: uint64(7 + i)},
  62. }); err != nil {
  63. t.Fatalf("insert %d: %v", i, err)
  64. }
  65. }
  66. rows, err := tbl.Query().OrderBy("Idx").Find()
  67. if err != nil {
  68. t.Fatalf("find: %v", err)
  69. }
  70. if len(rows) != len(vals) {
  71. t.Fatalf("应读出 %d 行, 得到 %d", len(vals), len(rows))
  72. }
  73. for i, row := range rows {
  74. if got := stringTrim(newTypeRowVal(t, row, "Wide")); got != vals[i] {
  75. t.Errorf("第 %d 行 Wide 长度 %d, 期望长度 %d (内容不一致)",
  76. i, len(got), len(vals[i]))
  77. }
  78. if got := stringTrim(newTypeRowVal(t, row, "Wide2")); got != vals[i] {
  79. t.Errorf("第 %d 行 Wide2 长度 %d, 期望长度 %d", i, len(got), len(vals[i]))
  80. }
  81. // 这两行才是"宽度正确"的真正证据
  82. if got := stringTrim(newTypeRowVal(t, row, "After")); got != after(i) {
  83. t.Errorf("第 %d 行 After = %q, 期望 %q —— 宽度算错时这里会先烂",
  84. i, got, after(i))
  85. }
  86. if got := newTypeRowVal(t, row, "Idx"); got != uint64(7+i) {
  87. t.Errorf("第 %d 行 Idx = %v, 期望 %d", i, got, 7+i)
  88. }
  89. }
  90. // 索引路径 (Wide) 与全表过滤路径 (Wide2) 必须给出同一个答案
  91. probe := vals[1]
  92. viaIdx, err := tbl.Query().Where(Eq("Wide", probe, types.FieldType(0))).Count()
  93. if err != nil {
  94. t.Fatalf("索引路径查询: %v", err)
  95. }
  96. viaScan, err := tbl.Query().Where(Eq("Wide2", probe, types.FieldType(0))).Count()
  97. if err != nil {
  98. t.Fatalf("全表过滤查询: %v", err)
  99. }
  100. if viaIdx != 1 || viaScan != 1 {
  101. t.Fatalf("索引路径 %d 行 / 全表过滤 %d 行, 都应为 1 (不一致即行内字节与索引键不同源)",
  102. viaIdx, viaScan)
  103. }
  104. // 定长列的比较是【整列宽】比较: 128 字节的 'b' 前缀不能命中 500 字节的行
  105. if n, _ := tbl.Query().Where(Eq("Wide", strings.Repeat("b", 128), types.FieldType(0))).Count(); n != 0 {
  106. t.Errorf("前缀 %q 不该命中 500 字节的行, 却命中了 %d 行 (比较没有按列宽补零)",
  107. strings.Repeat("b", 8)+"...", n)
  108. }
  109. // ===== 超宽: 报错, 不截断 (旧行为是静默截断: 写 20 字节的列声明 10 也会成功落库) =====
  110. before, _ := tbl.Query().Count()
  111. _, err = tbl.InsertRow([]types.FieldValue{
  112. {FieldName: []byte("ID"), Value: uint64(99)},
  113. {FieldName: []byte("Wide"), Value: strings.Repeat("d", width+1)},
  114. {FieldName: []byte("Wide2"), Value: "x"},
  115. {FieldName: []byte("After"), Value: after(99)},
  116. {FieldName: []byte("Idx"), Value: uint64(99)},
  117. })
  118. if !errors.Is(err, types.ErrValueRange) {
  119. t.Fatalf("写入 %d 字节 (列宽 %d) 应报 ErrValueRange, 得到 %v", width+1, width, err)
  120. }
  121. // 失败不能留下半条行, 也不能让表变得不可用
  122. if after, _ := tbl.Query().Count(); after != before {
  123. t.Errorf("写失败后行数从 %d 变成 %d: 失败的行有残留", before, after)
  124. }
  125. if n, _ := tbl.Query().Where(Eq("Idx", uint64(99), types.FieldType(0))).Count(); n != 0 {
  126. t.Errorf("被判超宽的行竟然进了表 (命中 %d 行)", n)
  127. }
  128. if _, err := tbl.InsertRow([]types.FieldValue{
  129. {FieldName: []byte("ID"), Value: uint64(100)},
  130. {FieldName: []byte("Wide"), Value: "ok"},
  131. {FieldName: []byte("Wide2"), Value: "ok"},
  132. {FieldName: []byte("After"), Value: after(100)},
  133. {FieldName: []byte("Idx"), Value: uint64(100)},
  134. }); err != nil {
  135. t.Errorf("超宽写入失败之后, 正常写入也失败了: %v", err)
  136. }
  137. }
  138. // TestRowSizeGate 行宽闸门: 建表 / 加列按【溢出下界】放行, 真正卡死的是"不可溢出列撑满"。
  139. //
  140. // 与旧语义的区别正在宽字符串列上: 它们可溢出, 所以 4 × 2048 = 8192 的【声明】字段区不再拒
  141. // (旧闸门会拒, 于是宽字符串表根本建不出来); 而一旦这些列【建了索引】就不能再溢出, 下界抬过
  142. // MaxInlineRowData 就必须拒 (ErrIndexRowTooWide)。
  143. func TestRowSizeGate(t *testing.T) {
  144. db := fixOpenDB(t)
  145. code := types.WidthCodeFor(types.MaxInlineColumnWidth) // 每列 2048 字节
  146. wide := func(name string) types.FieldInfo {
  147. return types.FieldInfo{
  148. FieldType: types.FieldType_String, Flag: code, FieldName: []byte(name),
  149. }
  150. }
  151. id := types.FieldInfo{FieldType: types.FieldType_Uint64, FieldName: []byte("id")}
  152. // 4 × 2048 = 8192 的声明字段区: 这些列都没建索引 ⇒ 都可溢出 ⇒ 允许建表 (旧闸门会拒)
  153. at, err := db.CreateTable("SPILLABLE", []types.FieldInfo{
  154. id, wide("A"), wide("B"), wide("C"), wide("D"),
  155. })
  156. if err != nil {
  157. t.Fatalf("4 × 2048 的可溢出字符串列应当允许建表: %v", err)
  158. }
  159. // 行存储索引 (主键): 下面"宽行仍能写入"必须真的落行, 否则会静默丢行
  160. if _, err := at.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  161. t.Fatalf("create pk: %v", err)
  162. }
  163. // 满值写入 (4 × 2048 = 8192 内容) 必须能落行且读得回: "装不下两行"的压力由值级溢出承担
  164. pk := at.AllocPKID()
  165. if _, err := at.InsertRowWithID(pk, []types.FieldValue{
  166. {FieldName: []byte("id"), Value: pk},
  167. {FieldName: []byte("A"), Value: strings.Repeat("a", 2048)},
  168. {FieldName: []byte("B"), Value: strings.Repeat("b", 2048)},
  169. {FieldName: []byte("C"), Value: strings.Repeat("c", 2048)},
  170. {FieldName: []byte("D"), Value: strings.Repeat("d", 2048)},
  171. }); err != nil {
  172. t.Fatalf("宽字符串行应当能写入 (靠溢出): %v", err)
  173. }
  174. if n, _ := at.Query().Count(); n != 1 {
  175. t.Fatalf("写入成功却读不到行 (读到 %d 行): 宽行没有真正落盘", n)
  176. }
  177. // 加列同理: 新列是可溢出字符串 ⇒ 放行 (加列闸门也按溢出下界算)
  178. if err := at.AddField(wide("E")); err != nil {
  179. t.Fatalf("加一列可溢出字符串应当放行: %v", err)
  180. }
  181. if _, present := at.GetActiveFieldType("E"); !present {
  182. t.Errorf("放行的新列 E 没进 schema")
  183. }
  184. // ===== 溢出闸门: 分量列建了索引就不能再溢出, 下界抬过上限必须拒 =====
  185. // id 是主键 (不可溢出)。再给 A/B/C 建索引后下界 = 1 + id8 + 3×2048 + D6 + E6 + 偏移表12
  186. // + 位图1 = 6178, 仍放行; 再给 D 建索引 ⇒ 4 列全宽 ⇒ 8220 > MaxInlineRowData, 必须拒。
  187. if _, err := at.CreateIndex("idx_a", page.IndexTypeBTree, []byte{1}); err != nil {
  188. t.Fatalf("给第 2 列建索引应当放行: %v", err)
  189. }
  190. if _, err := at.CreateIndex("idx_b", page.IndexTypeBTree, []byte{2}); err != nil {
  191. t.Fatalf("给第 3 列建索引应当放行: %v", err)
  192. }
  193. if _, err := at.CreateIndex("idx_c", page.IndexTypeBTree, []byte{3}); err != nil {
  194. t.Fatalf("给第 4 列建索引应当放行: %v", err)
  195. }
  196. if _, err := at.CreateIndex("idx_d", page.IndexTypeBTree, []byte{4}); !errors.Is(err, ErrIndexRowTooWide) {
  197. t.Fatalf("第 5 列再建索引会让不可溢出下界超 %d, 应当报 ErrIndexRowTooWide, 得到 %v",
  198. types.MaxInlineRowData, err)
  199. }
  200. }
  201. // TestMaxRowDataSizeFitsTwoRows 建列闸门推出的上限必须真的"一页两行"。
  202. //
  203. // 这是 types.MaxRowDataSize 的【唯一来由】(推导见那里的注释), 但那个上限算在 types 包里、
  204. // 用的是页面布局的镜像值, 所以在这里用真实的 PageSize / DataPageHdrSize 复核一遍: 镜像一旦
  205. // 与布局漂移, 上限就变成空头承诺 —— 建表时放行, 写第二行时才发现装不下。
  206. //
  207. // 顺带把 8192 钉在"拦得住"的那一侧: 8K 是个好看的数, 但每行还有固定开销与 NULL 位图,
  208. // 两行合计会越过页里能用的字节数 —— 旧上限就是这么来的, 这个断言防止它被改回去。
  209. func TestMaxRowDataSizeFitsTwoRows(t *testing.T) {
  210. // 一页能放行的行字节数: 末字节用不上 (InsertRowSortedByOffset 用 ">=" 提前判失败)
  211. avail := int(PageSize) - int(page.DataPageHdrSize) - 1
  212. // 上限本身: 两行正好装得下
  213. if 2*types.MaxRowDataSize > avail {
  214. t.Errorf("上限 %d 的两行 (%d 字节) 装不进一页 (%d)",
  215. types.MaxRowDataSize, 2*types.MaxRowDataSize, avail)
  216. }
  217. // 且它是满足该性质的【最大】值 —— 否则上限偏小, 白白少用页里一半空间
  218. if 2*(types.MaxRowDataSize+1) <= avail {
  219. t.Errorf("上限 %d 偏小: 再宽 1 字节的两行 (%d) 仍装得进一页 (%d)",
  220. types.MaxRowDataSize, 2*(types.MaxRowDataSize+1), avail)
  221. }
  222. // 旧上限 (8192 字节字段区): 加上每行开销后两行装不下 —— 它从来没兑现过这条政策
  223. oldWidestRow := 8192 + 4 + 8 + 1 + types.NullBitmapLen(1)
  224. if 2*oldWidestRow <= avail {
  225. t.Errorf("8192 字节字段区竟然装得下两行 (%d × 2 ≤ %d): 那 8192 本来就没问题",
  226. oldWidestRow, avail)
  227. }
  228. }
  229. // TestReviveFieldKeepsWidth 复活已删除的列时不允许改类型/宽度。
  230. //
  231. // master registry 里的类型与宽度码是【所有历史版本共用】的行偏移依据: 复活时把它换掉,
  232. // 已经落盘的旧行仍按旧宽度排布, 却会被按新宽度解析 —— 该列之后的字段全部静默错位。
  233. func TestReviveFieldKeepsWidth(t *testing.T) {
  234. db := fixOpenDB(t)
  235. tbl, err := db.CreateTable("REVIVE", []types.FieldInfo{
  236. {FieldType: types.FieldType_Uint64, FieldName: []byte("ID")},
  237. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(64), FieldName: []byte("Gone")},
  238. {FieldType: types.FieldType_Uint64, FieldName: []byte("Probe")},
  239. })
  240. if err != nil {
  241. t.Fatalf("create table: %v", err)
  242. }
  243. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  244. t.Fatalf("create pk: %v", err)
  245. }
  246. if _, err := tbl.InsertRow([]types.FieldValue{
  247. {FieldName: []byte("ID"), Value: uint64(1)},
  248. {FieldName: []byte("Gone"), Value: "keepme"},
  249. {FieldName: []byte("Probe"), Value: uint64(42)},
  250. }); err != nil {
  251. t.Fatalf("insert: %v", err)
  252. }
  253. if err := tbl.DropField("Gone"); err != nil {
  254. t.Fatalf("drop: %v", err)
  255. }
  256. err = tbl.AddField(types.FieldInfo{FieldType: types.FieldType_String, Flag: types.WidthCodeFor(32), FieldName: []byte("Gone")})
  257. if err == nil {
  258. t.Fatalf("复活时把 64 字节的列改成 32 字节必须被拒")
  259. }
  260. if !strings.Contains(err.Error(), "宽度") && !strings.Contains(err.Error(), "类型") {
  261. t.Errorf("拒因应说明是类型/宽度不一致: %v", err)
  262. }
  263. // 同类型复活: 允许, 且旧行 (按旧版本布局) 与新行 (按新版本布局) 都要读得对
  264. if err := tbl.AddField(types.FieldInfo{FieldType: types.FieldType_String, Flag: types.WidthCodeFor(64), FieldName: []byte("Gone")}); err != nil {
  265. t.Fatalf("同类型复活应当成功: %v", err)
  266. }
  267. if _, err := tbl.InsertRow([]types.FieldValue{
  268. {FieldName: []byte("ID"), Value: uint64(2)},
  269. {FieldName: []byte("Gone"), Value: "second"},
  270. {FieldName: []byte("Probe"), Value: uint64(43)},
  271. }); err != nil {
  272. t.Fatalf("复活后 insert: %v", err)
  273. }
  274. rows, err := tbl.Query().OrderBy("Probe").Find()
  275. if err != nil {
  276. t.Fatalf("find: %v", err)
  277. }
  278. if len(rows) != 2 {
  279. t.Fatalf("应读出 2 行, 得到 %d", len(rows))
  280. }
  281. wantGone := []string{"keepme", "second"}
  282. for i, row := range rows {
  283. if got := stringTrim(newTypeRowVal(t, row, "Gone")); got != wantGone[i] {
  284. t.Errorf("第 %d 行 Gone = %q, 期望 %q (旧行/新行用了不同版本的布局)",
  285. i, got, wantGone[i])
  286. }
  287. if got := newTypeRowVal(t, row, "Probe"); got != uint64(42+i) {
  288. t.Errorf("第 %d 行 Probe = %v, 期望 %d", i, got, 42+i)
  289. }
  290. }
  291. }