stringwidth_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package core
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "testing"
  8. "TsDB2/DB/core/page"
  9. "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_256byte, 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_8byte, FieldName: []byte("ID")},
  35. {FieldType: types.FieldType_256byte, Flag: code, FieldName: []byte("Wide")},
  36. // Wide2 与 Wide 同宽但【不建索引】: 用来比对"索引路径"与"全表过滤路径"
  37. {FieldType: types.FieldType_256byte, Flag: code, FieldName: []byte("Wide2")},
  38. // After / Idx 是错位探针: Wide 的宽度算错时先烂的是它们
  39. {FieldType: types.FieldType_64byte, FieldName: []byte("After")},
  40. {FieldType: types.FieldType_8byte, 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. // 判据不是"字段区 ≤ 8K": 4 × 2048 = 8192 的字段区看着正好 8K, 加上每行固定开销与 NULL
  141. // 位图之后就装不下两行了, 所以必须拒。
  142. func TestRowSizeGate(t *testing.T) {
  143. db := fixOpenDB(t)
  144. code := types.WidthCodeFor(types.MaxInlineColumnWidth) // 每列 2048 字节
  145. wide := func(name string) types.FieldInfo {
  146. return types.FieldInfo{
  147. FieldType: types.FieldType_256byte, Flag: code, FieldName: []byte(name),
  148. }
  149. }
  150. // 3 × 2048 = 6144 字段区: 加上开销仍装得下两行, 允许建表
  151. if _, err := db.CreateTable("NARROW", []types.FieldInfo{
  152. wide("A"), wide("B"), wide("C"),
  153. }); err != nil {
  154. t.Fatalf("3 × 2048 建表: %v", err)
  155. }
  156. at, ok := db.FindTable("NARROW")
  157. if !ok {
  158. t.Fatal("找不到 NARROW")
  159. }
  160. // 行存储索引 (主键): 下面"宽行仍能写入"必须真的落行, 否则 InsertRow 会静默丢行
  161. if _, err := at.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  162. t.Fatalf("create pk: %v", err)
  163. }
  164. // 6144 + 2048 = 8192 字段区: 一页装不下两行, AddField 必须被拒
  165. if err := at.AddField(wide("D")); err == nil {
  166. t.Errorf("4 × 2048 = 8192 的字段区装不下两行 (上限 %d), AddField 必须被拒", types.MaxRowDataSize)
  167. } else if !strings.Contains(err.Error(), "行宽") {
  168. t.Errorf("超宽错误应说明是行宽问题: %v", err)
  169. }
  170. // 被拒的列不能进 schema, 表本身也必须还能用
  171. if _, present := at.GetActiveFieldType("D"); present {
  172. t.Errorf("行宽超限的列 D 被加进了 schema")
  173. }
  174. if _, err := at.InsertRow([]types.FieldValue{
  175. {FieldName: []byte("A"), Value: "a"},
  176. {FieldName: []byte("B"), Value: "b"},
  177. {FieldName: []byte("C"), Value: "c"},
  178. }); err != nil {
  179. t.Errorf("闸门内的表应当仍能写入: %v", err)
  180. } else if n, _ := at.Query().Count(); n != 1 {
  181. t.Errorf("写入成功却读不到行 (读到 %d 行): 宽行没有真正落盘", n)
  182. }
  183. // 整表建的时候就超: 必须拒绝, 并列出各列宽度 (只说"超了"没法用, 用户要知道砍哪一列)
  184. _, err := db.CreateTable("OVER", []types.FieldInfo{
  185. wide("A"), wide("B"), wide("C"), wide("D"),
  186. })
  187. if err == nil {
  188. t.Fatalf("4 × 2048 = 8192 的字段区超过 %d, 必须拒绝建表", types.MaxRowDataSize)
  189. }
  190. for _, want := range []string{"行宽", "D=2048", "8192"} {
  191. if !strings.Contains(err.Error(), want) {
  192. t.Errorf("建表超宽的错误信息缺少 %q: %v", want, err)
  193. }
  194. }
  195. }
  196. // TestMaxRowDataSizeFitsTwoRows 建列闸门推出的上限必须真的"一页两行"。
  197. //
  198. // 这是 types.MaxRowDataSize 的【唯一来由】(推导见那里的注释), 但那个上限算在 types 包里、
  199. // 用的是页面布局的镜像值, 所以在这里用真实的 PageSize / DataPageHdrSize 复核一遍: 镜像一旦
  200. // 与布局漂移, 上限就变成空头承诺 —— 建表时放行, 写第二行时才发现装不下。
  201. //
  202. // 顺带把 8192 钉在"拦得住"的那一侧: 8K 是个好看的数, 但每行还有固定开销与 NULL 位图,
  203. // 两行合计会越过页里能用的字节数 —— 旧上限就是这么来的, 这个断言防止它被改回去。
  204. func TestMaxRowDataSizeFitsTwoRows(t *testing.T) {
  205. // 一页能放行的行字节数: 末字节用不上 (InsertRowSortedByOffset 用 ">=" 提前判失败)
  206. avail := int(PageSize) - int(page.DataPageHdrSize) - 1
  207. // 上限本身: 两行正好装得下
  208. if 2*types.MaxRowDataSize > avail {
  209. t.Errorf("上限 %d 的两行 (%d 字节) 装不进一页 (%d)",
  210. types.MaxRowDataSize, 2*types.MaxRowDataSize, avail)
  211. }
  212. // 且它是满足该性质的【最大】值 —— 否则上限偏小, 白白少用页里一半空间
  213. if 2*(types.MaxRowDataSize+1) <= avail {
  214. t.Errorf("上限 %d 偏小: 再宽 1 字节的两行 (%d) 仍装得进一页 (%d)",
  215. types.MaxRowDataSize, 2*(types.MaxRowDataSize+1), avail)
  216. }
  217. // 旧上限 (8192 字节字段区): 加上每行开销后两行装不下 —— 它从来没兑现过这条政策
  218. oldWidestRow := 8192 + 4 + 8 + 1 + types.NullBitmapLen(1)
  219. if 2*oldWidestRow <= avail {
  220. t.Errorf("8192 字节字段区竟然装得下两行 (%d × 2 ≤ %d): 那 8192 本来就没问题",
  221. oldWidestRow, avail)
  222. }
  223. }
  224. // TestReviveFieldKeepsWidth 复活已删除的列时不允许改类型/宽度。
  225. //
  226. // master registry 里的类型与宽度码是【所有历史版本共用】的行偏移依据: 复活时把它换掉,
  227. // 已经落盘的旧行仍按旧宽度排布, 却会被按新宽度解析 —— 该列之后的字段全部静默错位。
  228. func TestReviveFieldKeepsWidth(t *testing.T) {
  229. db := fixOpenDB(t)
  230. tbl, err := db.CreateTable("REVIVE", []types.FieldInfo{
  231. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  232. {FieldType: types.FieldType_64byte, FieldName: []byte("Gone")},
  233. {FieldType: types.FieldType_8byte, FieldName: []byte("Probe")},
  234. })
  235. if err != nil {
  236. t.Fatalf("create table: %v", err)
  237. }
  238. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  239. t.Fatalf("create pk: %v", err)
  240. }
  241. if _, err := tbl.InsertRow([]types.FieldValue{
  242. {FieldName: []byte("ID"), Value: uint64(1)},
  243. {FieldName: []byte("Gone"), Value: "keepme"},
  244. {FieldName: []byte("Probe"), Value: uint64(42)},
  245. }); err != nil {
  246. t.Fatalf("insert: %v", err)
  247. }
  248. if err := tbl.DropField("Gone"); err != nil {
  249. t.Fatalf("drop: %v", err)
  250. }
  251. err = tbl.AddField(types.FieldInfo{FieldType: types.FieldType_32byte, FieldName: []byte("Gone")})
  252. if err == nil {
  253. t.Fatalf("复活时把 64byte 的列改成 32byte 必须被拒")
  254. }
  255. if !strings.Contains(err.Error(), "宽度") && !strings.Contains(err.Error(), "类型") {
  256. t.Errorf("拒因应说明是类型/宽度不一致: %v", err)
  257. }
  258. // 同类型复活: 允许, 且旧行 (按旧版本布局) 与新行 (按新版本布局) 都要读得对
  259. if err := tbl.AddField(types.FieldInfo{FieldType: types.FieldType_64byte, FieldName: []byte("Gone")}); err != nil {
  260. t.Fatalf("同类型复活应当成功: %v", err)
  261. }
  262. if _, err := tbl.InsertRow([]types.FieldValue{
  263. {FieldName: []byte("ID"), Value: uint64(2)},
  264. {FieldName: []byte("Gone"), Value: "second"},
  265. {FieldName: []byte("Probe"), Value: uint64(43)},
  266. }); err != nil {
  267. t.Fatalf("复活后 insert: %v", err)
  268. }
  269. rows, err := tbl.Query().OrderBy("Probe").Find()
  270. if err != nil {
  271. t.Fatalf("find: %v", err)
  272. }
  273. if len(rows) != 2 {
  274. t.Fatalf("应读出 2 行, 得到 %d", len(rows))
  275. }
  276. wantGone := []string{"keepme", "second"}
  277. for i, row := range rows {
  278. if got := stringTrim(newTypeRowVal(t, row, "Gone")); got != wantGone[i] {
  279. t.Errorf("第 %d 行 Gone = %q, 期望 %q (旧行/新行用了不同版本的布局)",
  280. i, got, wantGone[i])
  281. }
  282. if got := newTypeRowVal(t, row, "Probe"); got != uint64(42+i) {
  283. t.Errorf("第 %d 行 Probe = %v, 期望 %d", i, got, 42+i)
  284. }
  285. }
  286. }