main.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. package main
  2. // 唯一索引 (IndexMeta.Flag 的 IndexFlagUnique 位) 的四条不变式。
  3. //
  4. // 1. 同一索引键只允许一行 —— 重复插入 / 改出重复都必须报 ErrDuplicatedKey;
  5. // 2. NULL 免除 —— 任一分量为 NULL 的行不进该索引, 因此多行 NULL 共存;
  6. // 3. 冲突必须在【任何写入之前】拦下 —— 被拒的行不能在行存储或别的索引里留下痕迹,
  7. // 被拒的更新不能让旧行变成"一半新一半旧";
  8. // 4. 建索引要校验存量 —— 已有重复值时唯一索引根本建不出来 (报错并摘掉元数据)。
  9. //
  10. // 第 3 条是这里最花篇幅的部分: 唯一性是"跨行"的约束, 而写入是逐索引进行的, 天然
  11. // 存在"查完才写"与"边写边查"两种实现 —— 后者在冲突时会把前几个索引写坏。
  12. //
  13. // 约束的执行者是【索引】, 列上的 types.FlagUnique 只是声明; 判定方式是写入前拿整条
  14. // 索引键探一次, 命中"别的行"即冲突 (命中本行自己的条目不算, 否则更新自身原值都会失败)。
  15. //
  16. // 运行: cd demo/unique_index_test && go run main.go
  17. import (
  18. "errors"
  19. "fmt"
  20. "os"
  21. "TsDB2/DB/core"
  22. "TsDB2/DB/core/page"
  23. "TsDB2/DB/core/types"
  24. )
  25. const dbPath = "unique_index_test.db"
  26. var passed, failed int
  27. // check 记一笔断言结果。
  28. func check(name string, ok bool, detail string) {
  29. if ok {
  30. passed++
  31. fmt.Printf(" ✅ %s\n", name)
  32. return
  33. }
  34. failed++
  35. fmt.Printf(" ❌ %s — %s\n", name, detail)
  36. }
  37. // checkErr 断言 err == nil; 返回是否通过 (setup 失败就不必往下走)。
  38. func checkErr(name string, err error) bool {
  39. if err != nil {
  40. check(name, false, err.Error())
  41. return false
  42. }
  43. check(name, true, "")
  44. return true
  45. }
  46. // checkDup 断言 err 命中 core.ErrDuplicatedKey。
  47. func checkDup(name string, err error) {
  48. check(name, errors.Is(err, core.ErrDuplicatedKey),
  49. fmt.Sprintf("err = %v, want ErrDuplicatedKey", err))
  50. }
  51. // fatal 建表 / 建索引失败属于环境问题, 直接中断 (不是被测行为)。
  52. func fatal(what string, err error) {
  53. if err != nil {
  54. fmt.Printf(" 💥 %s: %v\n", what, err)
  55. os.Exit(1)
  56. }
  57. }
  58. // idxErr 把 CreateIndex 的 (meta, err) 收敛成 err。
  59. func idxErr(_ *core.IndexMeta, err error) error { return err }
  60. // nv 构造一个 FieldValue; value 传 nil 即"显式写 NULL"。
  61. func nv(name string, value interface{}) types.FieldValue {
  62. return types.FieldValue{FieldName: []byte(name), Value: value}
  63. }
  64. // nvLookup 在结果行里找字段, 返回 (值, 是否存在)。
  65. func nvLookup(row []types.FieldValue, name string) (interface{}, bool) {
  66. for _, fv := range row {
  67. if string(fv.FieldName) == name {
  68. return fv.Value, true
  69. }
  70. }
  71. return nil, false
  72. }
  73. // checkUint 断言结果行里 field = want。
  74. func checkUint(name string, row []types.FieldValue, field string, want uint64) {
  75. v, ok := nvLookup(row, field)
  76. if !ok {
  77. check(name, false, fmt.Sprintf("字段 %s 缺失 (want %d)", field, want))
  78. return
  79. }
  80. got, ok := v.(uint64)
  81. check(name, ok && got == want, fmt.Sprintf("字段 %s = %v (%T), want %d", field, v, v, want))
  82. }
  83. // checkAbsent 断言字段没有出现在结果里 (NULL 的既定表示: 不 append)。
  84. func checkAbsent(name string, row []types.FieldValue, field string) {
  85. v, ok := nvLookup(row, field)
  86. check(name, !ok, fmt.Sprintf("字段 %s 应为 NULL 而不出现, 实得 %v", field, v))
  87. }
  88. // checkBytes 断言结果行里 field 的字节内容 = want。
  89. func checkBytes(name string, row []types.FieldValue, field string, want string) {
  90. v, ok := nvLookup(row, field)
  91. if !ok {
  92. check(name, false, fmt.Sprintf("字段 %s 缺失 (want %q)", field, want))
  93. return
  94. }
  95. b, ok := v.([]byte)
  96. check(name, ok && string(b) == want, fmt.Sprintf("字段 %s = %v (%T), want %q", field, v, v, want))
  97. }
  98. // checkCount 断言某列等值命中 want 行 (走该列的索引, 顺便验证索引本身是准的)。
  99. func checkCount(name string, tbl *core.Table, field string, v uint64, want int64) {
  100. n, err := tbl.Query().Where(core.Eq(field, v, types.FieldType(0))).Count()
  101. check(name, err == nil && n == want,
  102. fmt.Sprintf("%s=%d 命中 %d 行 (err=%v), want %d", field, v, n, err, want))
  103. }
  104. // insert 显式指定 pkID 插入一行。
  105. func insert(tbl *core.Table, pkID uint64, values []types.FieldValue) error {
  106. _, err := tbl.InsertRowWithID(pkID, values)
  107. return err
  108. }
  109. func main() {
  110. fmt.Println("==============================================")
  111. fmt.Println(" 唯一索引测试 (IndexMeta.Flag 唯一位 / NULL 免除)")
  112. fmt.Println("==============================================")
  113. os.Remove(dbPath)
  114. os.Remove(dbPath + ".wal")
  115. db, err := core.Open(dbPath)
  116. if err != nil {
  117. panic(err)
  118. }
  119. db.SetAutoCommitDelay(0) // 0 = 关闭自动提交, 只在显式 Commit 时落盘
  120. fmt.Println("\n---- 1. 重复值: 插入 / 更新都必须被拒, 且不留痕迹 ----")
  121. testRejectsDuplicateInsert(db)
  122. fmt.Println("\n---- 2. 被拒的更新不能动旧行 (含动态列) ----")
  123. testRejectedUpdateKeepsOldRow(db)
  124. fmt.Println("\n---- 3. NULL 免除: 多行 NULL 共存, 非 NULL 仍唯一 ----")
  125. testNullExemption(db)
  126. fmt.Println("\n---- 4. 复合唯一键: 约束的是整组分量 ----")
  127. testCompositeKey(db)
  128. fmt.Println("\n---- 5. 存量已有重复值时建不出唯一索引 ----")
  129. testBackfillOnExistingDuplicates(db)
  130. fmt.Println("\n---- 6. 行存储索引 (主键 / 时序) 不接受唯一约束 ----")
  131. testRowStoreRejectsUnique(db)
  132. _ = db.Close()
  133. fmt.Println()
  134. fmt.Println("==============================================")
  135. fmt.Printf("测试结果: %d 通过, %d 失败\n", passed, failed)
  136. if failed == 0 {
  137. fmt.Println("🎉 全部通过!")
  138. os.Remove(dbPath)
  139. os.Remove(dbPath + ".wal")
  140. } else {
  141. fmt.Printf("⚠️ 有 %d 项失败 (库文件保留在 %s 便于排查)\n", failed, dbPath)
  142. os.Exit(1)
  143. }
  144. }
  145. // uniqTable 建一张 ID/Tag/Grp 三列表: Tag 唯一索引, Grp 普通二级索引。
  146. //
  147. // Grp 这条普通索引是【半写探针】: 若唯一性检查发生在写入循环中途, 顺序在它之前的
  148. // 索引已经写入, 于是 Grp 索引里会留下一个指向不存在行的条目, 按 Grp 一查就是幽灵行。
  149. func uniqTable(db *core.DB) *core.Table {
  150. tbl, err := db.CreateTable("UNIQIDX", []types.FieldInfo{
  151. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  152. {FieldType: types.FieldType_8byte, FieldName: []byte("Tag")},
  153. {FieldType: types.FieldType_8byte, FieldName: []byte("Grp")},
  154. })
  155. fatal("建表 UNIQIDX", err)
  156. fatal("建主键索引 pk_uniq", idxErr(tbl.CreateIndex("pk_uniq", page.IndexTypePrimary, []byte{0})))
  157. fatal("建唯一索引 idx_uniq_tag", idxErr(tbl.CreateIndex("idx_uniq_tag", page.IndexTypeBTree, []byte{1}, true)))
  158. fatal("建普通索引 idx_uniq_grp", idxErr(tbl.CreateIndex("idx_uniq_grp", page.IndexTypeBTree, []byte{2})))
  159. return tbl
  160. }
  161. // uniqRow ID/Tag/Grp 一行。
  162. func uniqRow(id, tag, grp uint64) []types.FieldValue {
  163. return []types.FieldValue{nv("ID", id), nv("Tag", tag), nv("Grp", grp)}
  164. }
  165. func testRejectsDuplicateInsert(db *core.DB) {
  166. tbl := uniqTable(db)
  167. checkErr("插入 row1 (Tag=10)", insert(tbl, 1, uniqRow(1, 10, 100)))
  168. // 重复 Tag → 拒绝
  169. checkDup("重复 Tag 插入被拒", insert(tbl, 2, uniqRow(2, 10, 200)))
  170. // 被拒的行不能留下痕迹: 行存储里没有, 普通索引里也不能有幽灵条目。
  171. _, err := tbl.FindByPK(2)
  172. check("被拒的行不在行存储里 (冲突必须早于任何写入)", err != nil, "FindByPK(2) 竟然找到了行")
  173. checkCount("被拒的行没在普通索引里留幽灵条目", tbl, "Grp", 200, 0)
  174. if n, err := tbl.Query().Count(); err != nil || n != 1 {
  175. check("表里仍只有 1 行", false, fmt.Sprintf("行数 = %d (err=%v), want 1", n, err))
  176. } else {
  177. check("表里仍只有 1 行", true, "")
  178. }
  179. // ===== 更新: 改成已被占用的值 → 拒绝, 且原值原封不动 =====
  180. checkErr("把 row1 的 Tag 改成 20 (释放 10)", tbl.UpdateRowByPK(1, []types.FieldValue{nv("Tag", uint64(20))}))
  181. checkErr("Tag=10 释放后可以插入", insert(tbl, 2, uniqRow(2, 10, 200)))
  182. checkDup("把 row2 的 Tag 改成 20 (撞 row1) 被拒", tbl.UpdateRowByPK(2, []types.FieldValue{nv("Tag", uint64(20))}))
  183. row2, err := tbl.FindByPK(2)
  184. if checkErr("读回 row2", err) {
  185. checkUint("被拒后 row2.Tag 仍是 10 (不能删了旧的没写新的)", row2, "Tag", 10)
  186. checkUint("被拒后 row2.Grp 也没动", row2, "Grp", 200)
  187. }
  188. // 改成自己的原值不算冲突 (命中的是本行自己的索引条目)
  189. checkErr("改回自身原值不该报冲突", tbl.UpdateRowByPK(2, []types.FieldValue{nv("Tag", uint64(10))}))
  190. // 换成没人占用的值 → 应成功, 且旧键要从索引里消失
  191. checkErr("把 row2 的 Tag 改成 30", tbl.UpdateRowByPK(2, []types.FieldValue{nv("Tag", uint64(30))}))
  192. checkCount("离开的旧键已从索引里摘掉", tbl, "Tag", 10, 0)
  193. checkCount("新键已进索引", tbl, "Tag", 30, 1)
  194. // 全表走一遍: 值必须与索引一致
  195. rows, err := tbl.Query().Find()
  196. if !checkErr("全表扫描", err) {
  197. return
  198. }
  199. tags := map[uint64]int{}
  200. for _, r := range rows {
  201. v, _ := nvLookup(r, "Tag")
  202. tags[v.(uint64)]++
  203. }
  204. check("全表 Tag 分布 = {20:1, 30:1}", len(rows) == 2 && tags[20] == 1 && tags[30] == 1,
  205. fmt.Sprintf("实际 %v (%d 行)", tags, len(rows)))
  206. }
  207. // testRejectedUpdateKeepsOldRow 被拒的更新必须"要么全改, 要么全不动"。
  208. //
  209. // Body 是动态列 (堆块), 它正是这条不变式最脆弱的地方: serializeRow 会就地覆写旧行的
  210. // 堆块 (heapRewrite) 甚至 heapUnref 掉旧块, 而索引冲突是在那之后才由 UpdateByKey 发现。
  211. // 若预检不放序列化之前, 这里读回的 Body 就会是新值 (极端情况是已释放的块)。
  212. func testRejectedUpdateKeepsOldRow(db *core.DB) {
  213. tbl, err := db.CreateTable("UNIQDYN", []types.FieldInfo{
  214. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  215. {FieldType: types.FieldType_8byte, FieldName: []byte("Tag")},
  216. {FieldType: types.FieldType_Dynamicbyte, FieldName: []byte("Body")},
  217. })
  218. fatal("建表 UNIQDYN", err)
  219. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_uniqdyn", page.IndexTypePrimary, []byte{0})))
  220. fatal("建唯一索引", idxErr(tbl.CreateIndex("idx_uniqdyn_tag", page.IndexTypeBTree, []byte{1}, true)))
  221. checkErr("插入 row1 (Tag=10, Body=aaa)", insert(tbl, 1, []types.FieldValue{
  222. nv("ID", uint64(1)), nv("Tag", uint64(10)), nv("Body", []byte("aaa")),
  223. }))
  224. // row2 的 Body 刻意留长: 这次被拒的更新里新值更短, 会命中 heapRewrite 的原地覆写
  225. // 分支 (oldLen >= newLen) —— 若预检晚于 serializeRow, 旧行的堆块内容会立刻被改成
  226. // 新值, 读回来就是脏的 (新值更长时走换块, 表现为悬空引用, 要等那块内存被重新分配
  227. // 出去才露出来 —— 更难测, 所以这里选"变短")。
  228. checkErr("插入 row2 (Tag=20, Body=bbbbb)", insert(tbl, 2, []types.FieldValue{
  229. nv("ID", uint64(2)), nv("Tag", uint64(20)), nv("Body", []byte("bbbbb")),
  230. }))
  231. // 同时改 Tag (撞车) 与 Body: 冲突必须让整次更新作废, Body 也得原样。
  232. checkDup("同时改 Tag(撞车) 与 Body 被拒",
  233. tbl.UpdateRowByPK(2, []types.FieldValue{nv("Tag", uint64(10)), nv("Body", []byte("c"))}))
  234. row2, err := tbl.FindByPK(2)
  235. if !checkErr("读回 row2", err) {
  236. return
  237. }
  238. checkUint("被拒后 row2.Tag 仍是 20", row2, "Tag", 20)
  239. checkBytes("被拒后 row2.Body 仍是 bbbbb (没被就地覆写)", row2, "Body", "bbbbb")
  240. }
  241. func testNullExemption(db *core.DB) {
  242. tbl, err := db.CreateTable("UNIQNULL", []types.FieldInfo{
  243. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  244. {FieldType: types.FieldType_8byte, FieldName: []byte("Note"), Flag: types.FlagNullable},
  245. })
  246. fatal("建表 UNIQNULL", err)
  247. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_uniqnull", page.IndexTypePrimary, []byte{0})))
  248. fatal("建唯一索引", idxErr(tbl.CreateIndex("idx_uniqnull_note", page.IndexTypeBTree, []byte{1}, true)))
  249. // 三行 NULL: 唯一索引一条条目都没有 → 随便多少行都行
  250. for i := uint64(1); i <= 3; i++ {
  251. checkErr(fmt.Sprintf("插入第 %d 行 (Note=NULL)", i),
  252. insert(tbl, i, []types.FieldValue{nv("ID", i), nv("Note", nil)}))
  253. }
  254. checkCount("NULL 不等于 0 (Note=0 命中 0 行)", tbl, "Note", 0, 0)
  255. // 非 NULL 值仍然唯一: 第一行 7 成功, 第二行 7 拒绝
  256. checkErr("插入 Note=7", insert(tbl, 4, []types.FieldValue{nv("ID", uint64(4)), nv("Note", uint64(7))}))
  257. checkDup("第二个 Note=7 被拒",
  258. insert(tbl, 5, []types.FieldValue{nv("ID", uint64(5)), nv("Note", uint64(7))}))
  259. // NULL 行改成 7 → 也撞车 (NULL 行不在索引里, 但 7 已被占)
  260. checkDup("NULL → 7 被拒 (7 已被占)",
  261. tbl.UpdateRowByPK(1, []types.FieldValue{nv("Note", uint64(7))}))
  262. row1, err := tbl.FindByPK(1)
  263. if checkErr("读回 row1", err) {
  264. checkAbsent("被拒后 row1.Note 仍是 NULL", row1, "Note")
  265. }
  266. // 持值行改成 NULL → 成功, 索引条目必须同步摘掉
  267. checkErr("7 → NULL 成功", tbl.UpdateRowByPK(4, []types.FieldValue{nv("Note", nil)}))
  268. checkCount("值改成 NULL 后索引条目已摘掉", tbl, "Note", 7, 0)
  269. checkErr("7 腾出来后新的 7 可以进",
  270. insert(tbl, 6, []types.FieldValue{nv("ID", uint64(6)), nv("Note", uint64(7))}))
  271. checkCount("Note=7 命中 1 行", tbl, "Note", 7, 1)
  272. }
  273. // testCompositeKey 复合唯一索引: 唯一性作用于【整组分量】。
  274. //
  275. // 与单列版的差别正是最容易写错的地方:
  276. // - 只有一个分量不同就是不同的键 (10,200) 与 (10,100) 可以共存;
  277. // - 整组相同才算撞车, 且撞车判定要用【整个键】去探, 漏掉尾部分量就会把
  278. // "A 相同"误判成重复, 白白拒掉合法数据;
  279. // - NULL 免除同样按"任一分量为 NULL"生效: (10,NULL) 的行整行不进索引, 可以有多行。
  280. func testCompositeKey(db *core.DB) {
  281. tbl, err := db.CreateTable("UNIQCOMP", []types.FieldInfo{
  282. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  283. {FieldType: types.FieldType_8byte, FieldName: []byte("A")},
  284. {FieldType: types.FieldType_8byte, FieldName: []byte("B"), Flag: types.FlagNullable},
  285. })
  286. fatal("建表 UNIQCOMP", err)
  287. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_uniqcomp", page.IndexTypePrimary, []byte{0})))
  288. m, err := tbl.CreateIndex("idx_uniqcomp_ab", page.IndexTypeBTree, []byte{1, 2}, true)
  289. fatal("建复合唯一索引", err)
  290. check("复合索引带着唯一位 (2 个分量)", m.Unique() && m.FieldCnt == 2,
  291. fmt.Sprintf("Unique=%v FieldCnt=%d, want true/2", m.Unique(), m.FieldCnt))
  292. ab := func(id uint64, a uint64, b interface{}) []types.FieldValue {
  293. return []types.FieldValue{nv("ID", id), nv("A", a), nv("B", b)}
  294. }
  295. // 尾部分量不同 → 不同的键, 都该进得去
  296. checkErr("插入 (A=10,B=100)", insert(tbl, 1, ab(1, 10, uint64(100))))
  297. checkErr("插入 (A=10,B=200): 与 (10,100) 是不同键", insert(tbl, 2, ab(2, 10, uint64(200))))
  298. checkErr("插入 (A=20,B=100): 与 (10,100) 是不同键", insert(tbl, 3, ab(3, 20, uint64(100))))
  299. // 整组相同 → 撞车
  300. checkDup("重复 (10,100) 被拒", insert(tbl, 4, ab(4, 10, uint64(100))))
  301. // 任一分量 NULL → 整行不进索引, 多行共存
  302. checkErr("插入 (A=10,B=NULL)", insert(tbl, 4, ab(4, 10, nil)))
  303. checkErr("第二行 (A=10,B=NULL) 不冲突 (NULL 免除)", insert(tbl, 5, ab(5, 10, nil)))
  304. // 更新到已被占用的整组键 → 拒绝; 改 A 或 B 任一即可避开
  305. checkDup("更新成 (10,100) 被拒",
  306. tbl.UpdateRowByPK(3, []types.FieldValue{nv("A", uint64(10)), nv("B", uint64(100))}))
  307. row3, err := tbl.FindByPK(3)
  308. if checkErr("读回 row3", err) {
  309. checkUint("被拒后 row3.A 仍是 20", row3, "A", 20)
  310. checkUint("被拒后 row3.B 仍是 100", row3, "B", 100)
  311. }
  312. // 只改 B: (20,300) 无人占用 → 成功
  313. checkErr("更新 (20,100) → (20,300)",
  314. tbl.UpdateRowByPK(3, []types.FieldValue{nv("B", uint64(300))}))
  315. // A=10 走这个复合索引只能找到 2 行: 另外两行 B 是 NULL, 整行压根不在索引里。
  316. // 这正是"NULL 免除"的物理表现 —— 它们只能靠全表扫描 / 主键找到。
  317. checkCount("A=10 经复合索引命中 2 行 (B=NULL 的两行不在索引里)", tbl, "A", 10, 2)
  318. rows, err := tbl.Query().Find()
  319. if !checkErr("全表扫描", err) {
  320. return
  321. }
  322. check("总行数 = 5 (两次被拒的插入/更新都没留下痕迹)", len(rows) == 5,
  323. fmt.Sprintf("行数 = %d, want 5", len(rows)))
  324. }
  325. // testBackfillOnExistingDuplicates 存量已有重复值时, 唯一索引必须建不出来。
  326. //
  327. // 硬建下去会得到一个"违反自身约束"的索引 (同一键挂两行), 点查可能返回其中任意一行,
  328. // 而索引看起来完全正常 —— 这类坏索引无法从外部察觉, 只能在建立时拦下。
  329. func testBackfillOnExistingDuplicates(db *core.DB) {
  330. tbl, err := db.CreateTable("UNIQBAK", []types.FieldInfo{
  331. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  332. {FieldType: types.FieldType_8byte, FieldName: []byte("Tag")},
  333. })
  334. fatal("建表 UNIQBAK", err)
  335. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_uniqbak", page.IndexTypePrimary, []byte{0})))
  336. // 先插两行同值 (此时还没有唯一索引, 允许)
  337. checkErr("插入 row1 (Tag=10)", insert(tbl, 1, []types.FieldValue{nv("ID", uint64(1)), nv("Tag", uint64(10))}))
  338. checkErr("插入 row2 (Tag=10)", insert(tbl, 2, []types.FieldValue{nv("ID", uint64(2)), nv("Tag", uint64(10))}))
  339. _, err = tbl.CreateIndex("idx_uniqbak_tag", page.IndexTypeBTree, []byte{1}, true)
  340. checkDup("存量重复时建唯一索引被拒", err)
  341. // 失败的索引不能留在元数据里 (半个索引比没有索引更危险: 查询会选中它)
  342. _, found := tbl.IndexMeta().FindIndex("idx_uniqbak_tag")
  343. check("建立失败的唯一索引没留在元数据里", !found, "索引仍在元数据里")
  344. // 把重复行删掉之后就该能建成功
  345. checkErr("删掉 row2", tbl.DeleteRowByPK(2))
  346. fatal("清掉重复后建唯一索引", idxErr(tbl.CreateIndex("idx_uniqbak_tag", page.IndexTypeBTree, []byte{1}, true)))
  347. // 建好之后约束立即生效
  348. checkDup("回填后插入重复值被拒", insert(tbl, 3, []types.FieldValue{nv("ID", uint64(3)), nv("Tag", uint64(10))}))
  349. }
  350. // testRowStoreRejectsUnique 行存储索引不接受唯一约束 —— 必须报错而不是静默忽略。
  351. //
  352. // 主键本就唯一, 时序索引的多行同刻是【刻意允许】的。静默忽略这个标记最坏: 调用方
  353. // 以为约束生效了, 直到插入同刻第二行成功才发现不对, 那时数据已经写进去了。
  354. func testRowStoreRejectsUnique(db *core.DB) {
  355. tbl, err := db.CreateTable("UNIQRS", []types.FieldInfo{
  356. {FieldType: types.FieldType_8byte, FieldName: []byte("ID")},
  357. })
  358. fatal("建表 UNIQRS", err)
  359. _, err = tbl.CreateIndex("pk_uniqrs", page.IndexTypePrimary, []byte{0}, true)
  360. check("行存储索引标唯一被拒 (ErrUniqueRowStore)", errors.Is(err, core.ErrUniqueRowStore),
  361. fmt.Sprintf("err = %v, want ErrUniqueRowStore", err))
  362. // 非唯一仍然可以建
  363. fatal("普通主键索引", idxErr(tbl.CreateIndex("pk_uniqrs", page.IndexTypePrimary, []byte{0})))
  364. m, found := tbl.IndexMeta().FindIndex("pk_uniqrs")
  365. check("普通主键索引没被标成唯一", found && !m.Unique(), fmt.Sprintf("found=%v", found))
  366. }