main.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package main
  2. // ORM 侧 `vrtx:"unique"` 的语义链路:
  3. //
  4. // tag → 列级 types.FlagUnique + 索引级 core.IndexFlagUnique
  5. // → 索引写入路径 (InsertRow / UpdateByKey) 上校验
  6. //
  7. // 三条不变式:
  8. // 1. 同一列值只能出现一次 (重复插入报 core.ErrDuplicatedKey);
  9. // 2. NULL 不参与 —— 可空列的多行 NULL 共存 (NULL 行不进唯一索引, 索引里没有条目可撞);
  10. // 3. 模型声明 unique 而库里那个索引不是唯一的, 重开表时必须报错 —— 否则
  11. // "模型说唯一、写入却允许重复", 要等数据脏了才被发现。
  12. //
  13. // 顺带钉住指针载体: `*int64` 推断出的列型与 `int64` 一致 (指针只是多了"整列为空"
  14. // 这一档), nil 写 NULL、非 nil 解一层指针按值写, 读回仍是 nil / 7 而不是 0 / 7。
  15. //
  16. // 运行: cd demo/unique_orm_test && go run main.go
  17. import (
  18. "errors"
  19. "fmt"
  20. "os"
  21. "TsDB2/DB"
  22. "TsDB2/DB/core"
  23. "TsDB2/DB/core/types"
  24. )
  25. // ============================ 模型 ============================
  26. // ormUniq 唯一约束模型:
  27. //
  28. // Name int64 `vrtx:"unique"` — 非空, 值唯一
  29. // Email *int64 `vrtx:"unique,nullable"` — 可空, 值唯一, 多行 NULL 共存
  30. // Group int64 `vrtx:"index"` — 普通索引 (对照组)
  31. type ormUniq struct {
  32. DB.BaseModel
  33. Name int64 `vrtx:"unique"`
  34. Email *int64 `vrtx:"unique,nullable"`
  35. Group int64 `vrtx:"index"`
  36. }
  37. // ormUniqLax 列集合与索引名和 ormUniq 完全一致, 只是都不声明 unique ——
  38. // 先用它建表再用 ormUniq 重开, 就构造出"索引已存在但不是唯一"的场景。
  39. type ormUniqLax struct {
  40. DB.BaseModel
  41. Name int64 `vrtx:"index"`
  42. Email *int64 `vrtx:"index,nullable"`
  43. Group int64 `vrtx:"index"`
  44. }
  45. // ormUniqPair 复合唯一键: index:u2 把两列并成一组, unique 只写在 A 上 ——
  46. // 唯一性作用于 (A,B) 整组, 而不是 A 单列。
  47. type ormUniqPair struct {
  48. DB.BaseModel
  49. A int64 `vrtx:"index:u2,unique"`
  50. B int64 `vrtx:"index:u2"`
  51. Note int64 `vrtx:"nullable"`
  52. }
  53. func i64ptr(v int64) *int64 { return &v }
  54. // ============================ 断言工具 ============================
  55. var passed, failed int
  56. // check 记一笔断言结果; 返回是否通过 (便于"前提不成立就不往下走")。
  57. func check(name string, ok bool, detail string) bool {
  58. if ok {
  59. passed++
  60. fmt.Printf(" ✅ %s\n", name)
  61. return true
  62. }
  63. failed++
  64. fmt.Printf(" ❌ %s — %s\n", name, detail)
  65. return false
  66. }
  67. // checkErr 断言 err == nil; 返回是否通过。
  68. func checkErr(name string, err error) bool {
  69. if err != nil {
  70. check(name, false, err.Error())
  71. return false
  72. }
  73. check(name, true, "")
  74. return true
  75. }
  76. // checkDup 断言 err 命中 core.ErrDuplicatedKey。
  77. func checkDup(name string, err error) {
  78. check(name, errors.Is(err, core.ErrDuplicatedKey),
  79. fmt.Sprintf("err = %v, want ErrDuplicatedKey", err))
  80. }
  81. // fatal 环境问题 (建库 / 建表) 直接中断。
  82. func fatal(what string, err error) {
  83. if err != nil {
  84. fmt.Printf(" 💥 %s: %v\n", what, err)
  85. os.Exit(1)
  86. }
  87. }
  88. // openDemoDB 打开本程序专用的库文件 (先删旧的)。
  89. // 清理时若已有失败项就保留文件, 便于用 analyzer 排查。
  90. func openDemoDB(path string) (*DB.DB, func()) {
  91. os.Remove(path)
  92. os.Remove(path + ".wal")
  93. db, err := DB.Open(path)
  94. if err != nil {
  95. panic(err)
  96. }
  97. db.SetAutoCommitDelay(0) // 0 = 关闭自动提交, 只在显式 Commit 时落盘
  98. return db, func() {
  99. _ = db.Close()
  100. if failed == 0 {
  101. os.Remove(path)
  102. os.Remove(path + ".wal")
  103. }
  104. }
  105. }
  106. // coreTbl 拿到底层 core.Table —— ORM 层不暴露索引元数据, 唯一位得从这里看。
  107. func coreTbl(db *DB.DB, name string) *core.Table {
  108. t, ok := db.CoreDB().FindTable(name)
  109. if !ok {
  110. panic("core 里找不到表: " + name)
  111. }
  112. return t
  113. }
  114. // findIdx 按名字取出模型里的索引定义。
  115. func findIdx(m *DB.ModelInfo, name string) (DB.IndexDef, bool) {
  116. for _, idx := range m.Indexes {
  117. if idx.Name == name {
  118. return idx, true
  119. }
  120. }
  121. return DB.IndexDef{}, false
  122. }
  123. // idxFields 索引的分量列表 (单列索引只有 FieldID, 复合索引看 FieldIDs)。
  124. func idxFields(d DB.IndexDef) []byte {
  125. if len(d.FieldIDs) > 0 {
  126. return d.FieldIDs
  127. }
  128. return []byte{d.FieldID}
  129. }
  130. func main() {
  131. fmt.Println("==============================================")
  132. fmt.Println(" ORM 唯一约束测试 (vrtx:\"unique\")")
  133. fmt.Println("==============================================")
  134. fmt.Println("\n---- 1. 标签布线: tag → 列标志 / 索引唯一位 ----")
  135. testTagWiring()
  136. fmt.Println("\n---- 2. 端到端: 写入 / 读回 / 重开表 ----")
  137. testEndToEnd()
  138. fmt.Println("\n---- 3. 库里已有同名非唯一索引时必须报错 ----")
  139. testReopenWithStaleIndex()
  140. fmt.Println()
  141. fmt.Println("==============================================")
  142. fmt.Printf("测试结果: %d 通过, %d 失败\n", passed, failed)
  143. if failed == 0 {
  144. fmt.Println("🎉 全部通过!")
  145. } else {
  146. fmt.Printf("⚠️ 有 %d 项失败\n", failed)
  147. os.Exit(1)
  148. }
  149. }
  150. // testTagWiring 先钉住"tag → 列标志 / 索引唯一标记"的映射, 不碰磁盘。
  151. func testTagWiring() {
  152. m, err := DB.ParseModel(&ormUniq{})
  153. fatal("ParseModel(ormUniq)", err)
  154. cols := map[string]DB.FieldDef{}
  155. for _, f := range m.Fields {
  156. cols[f.ColName] = f
  157. }
  158. check("Name 列带 FlagUnique", cols["Name"].Flag&types.FlagUnique != 0,
  159. fmt.Sprintf("Flag = %#02x, 缺 FlagUnique", cols["Name"].Flag))
  160. check("Email 列带 FlagUnique|FlagNullable",
  161. cols["Email"].Flag&types.FlagUnique != 0 && cols["Email"].Flag&types.FlagNullable != 0,
  162. fmt.Sprintf("Flag = %#02x, 需要 FlagUnique|FlagNullable", cols["Email"].Flag))
  163. check("Group 列没有 FlagUnique", cols["Group"].Flag&types.FlagUnique == 0,
  164. fmt.Sprintf("Flag = %#02x", cols["Group"].Flag))
  165. // 指针载体与值载体的列型必须一致: 指针只是多了"整列为空"这一档。
  166. check("*int64 与 int64 推断出同一个列型", cols["Email"].FieldType == cols["Name"].FieldType,
  167. fmt.Sprintf("*int64 → %d, int64 → %d", cols["Email"].FieldType, cols["Name"].FieldType))
  168. // 索引侧: 唯一标记落在索引上 (约束的执行者), Name 与 Email 各一个。
  169. //
  170. // 这里按【列】找索引, 而不是按名字: ParseModel 此刻还不知道表名, 自动索引名里的
  171. // 表名部分要等 UseTable 才定下来 (所以名字在端到端那段里核对)。
  172. uniqByCol := map[string]bool{}
  173. for _, idx := range m.Indexes {
  174. fids := idxFields(idx)
  175. if len(fids) != 1 {
  176. check("每个索引各 1 个分量", false, fmt.Sprintf("索引 %s 分量数 = %d", idx.Name, len(fids)))
  177. continue
  178. }
  179. uniqByCol[m.AllFields[fids[0]].ColName] = idx.Unique
  180. }
  181. for _, col := range []string{"Name", "Email"} {
  182. check("列 "+col+" 的索引带唯一位", uniqByCol[col], "没有唯一标记")
  183. }
  184. check("列 Group 的索引不带唯一位", !uniqByCol["Group"], "被标成了唯一")
  185. // ===== 复合唯一: unique 写在 A 上, 约束的是 (A,B) 整组 =====
  186. pm, err := DB.ParseModel(&ormUniqPair{})
  187. fatal("ParseModel(ormUniqPair)", err)
  188. u2, ok := findIdx(pm, "u2")
  189. if !check("复合索引 u2 已生成", ok, "模型里没有 u2") {
  190. return
  191. }
  192. check("u2 是唯一索引 (unique 只写在 A 上, 唯一性属于整组)", u2.Unique, "u2 不是唯一索引")
  193. check("u2 有 2 个分量", len(idxFields(u2)) == 2,
  194. fmt.Sprintf("分量数 = %d, want 2", len(idxFields(u2))))
  195. pc := map[string]DB.FieldDef{}
  196. for _, f := range pm.Fields {
  197. pc[f.ColName] = f
  198. }
  199. // 位只反映"这一列 tag 里写了 unique": A 有, B 没有。
  200. //
  201. // 刻意【不】往 B 上补齐: (A,B) 的唯一键不等于 B 唯一, 给 B 打上唯一位就成了
  202. // "这一列单独唯一"的假话; 而照位反推 tag 的工具会给 B 补一个 unique, 那在 ORM 里
  203. // 意味着"给 B 单独建一个唯一索引", 凭空多出一条约束。
  204. check("A 列带 FlagUnique", pc["A"].Flag&types.FlagUnique != 0,
  205. fmt.Sprintf("Flag = %#02x", pc["A"].Flag))
  206. check("B 列没有 FlagUnique (唯一性是整组的)", pc["B"].Flag&types.FlagUnique == 0,
  207. fmt.Sprintf("Flag = %#02x", pc["B"].Flag))
  208. check("Note 列带 FlagNullable", pc["Note"].Flag&types.FlagNullable != 0,
  209. fmt.Sprintf("Flag = %#02x", pc["Note"].Flag))
  210. }
  211. // testEndToEnd 唯一约束在真实的写入 / 读取 / 重开表链路上都必须成立。
  212. func testEndToEnd() {
  213. db, cleanup := openDemoDB("unique_orm.db")
  214. defer cleanup()
  215. tbl, err := DB.UseTable[ormUniq](db, "uniq")
  216. fatal("UseTable[ormUniq]", err)
  217. // 列标志与索引标记都要真的落到 core schema 上 (否则约束只是纸面声明)。
  218. f, ok := coreTbl(db, "uniq").GetActiveFieldFlag("Name")
  219. check("core schema 的 Name 列带 FlagUnique", ok && f&types.FlagUnique != 0,
  220. fmt.Sprintf("Flag = %#02x (ok=%v), 缺 FlagUnique", f, ok))
  221. for _, name := range []string{"idx_uniq_Name", "idx_uniq_Email"} {
  222. m, found := coreTbl(db, "uniq").IndexMeta().FindIndex(name)
  223. check("core 里 "+name+" 是唯一索引", found && m.Unique(), fmt.Sprintf("found=%v", found))
  224. }
  225. // ===== NULL 免除: 多行 NULL 共存 =====
  226. for i := int64(1); i <= 3; i++ {
  227. checkErr(fmt.Sprintf("插入第 %d 行 (Email=NULL)", i),
  228. tbl.Insert(&ormUniq{Name: i, Email: nil, Group: 100}))
  229. }
  230. // ===== 非 NULL 值唯一 =====
  231. checkErr("插入 Email=7", tbl.Insert(&ormUniq{Name: 4, Email: i64ptr(7), Group: 100}))
  232. checkDup("第二个 Email=7 被拒", tbl.Insert(&ormUniq{Name: 5, Email: i64ptr(7), Group: 100}))
  233. // Name 也是唯一列 (非主键): 重复 Name 同样被拒
  234. checkDup("重复 Name 被拒", tbl.Insert(&ormUniq{Name: 1, Email: nil, Group: 100}))
  235. // ===== 读回: 可空列按指针载体原样往返 =====
  236. rows, err := tbl.Where("Email", "=", int64(7)).Find()
  237. if checkErr("查 Email=7", err) {
  238. if len(rows) != 1 {
  239. check("Email=7 命中 1 行", false, fmt.Sprintf("命中 %d 行", len(rows)))
  240. } else {
  241. check("Email=7 命中 1 行且指针解回 7",
  242. rows[0].Email != nil && *rows[0].Email == 7 && rows[0].Name == 4,
  243. fmt.Sprintf("Email = %v, Name = %d", rows[0].Email, rows[0].Name))
  244. }
  245. }
  246. // NULL 行读回必须是 nil 指针 (不是 0): 0 是"读到了 0", nil 才是"没有值"。
  247. nulls, err := tbl.Where("Name", "=", int64(1)).Find()
  248. if checkErr("查 Name=1", err) {
  249. if len(nulls) != 1 {
  250. check("Name=1 命中 1 行", false, fmt.Sprintf("命中 %d 行", len(nulls)))
  251. } else {
  252. check("NULL 读回是 nil 指针 (不是 0)", nulls[0].Email == nil,
  253. fmt.Sprintf("Email = %v", nulls[0].Email))
  254. }
  255. }
  256. // ===== 重开表: 约束必须还在 (它随索引元数据落盘) =====
  257. fatal("Commit", db.Commit())
  258. tbl2, err := DB.UseTable[ormUniq](db, "uniq")
  259. if !checkErr("重开表 UseTable[ormUniq]", err) {
  260. return
  261. }
  262. checkDup("重开后插入重复值仍被拒", tbl2.Insert(&ormUniq{Name: 6, Email: i64ptr(7), Group: 100}))
  263. checkErr("重开后 NULL 行仍然可以进 (约束没把 NULL 也管上)",
  264. tbl2.Insert(&ormUniq{Name: 6, Email: nil, Group: 100}))
  265. }
  266. // testReopenWithStaleIndex 库里已有同名非唯一索引时, 声明 unique 必须报错。
  267. func testReopenWithStaleIndex() {
  268. db, cleanup := openDemoDB("unique_orm_stale.db")
  269. defer cleanup()
  270. // 先用"不声明 unique"的模型建表 (索引名与分量和 ormUniq 完全一致)
  271. _, err := DB.UseTable[ormUniqLax](db, "uniq")
  272. fatal("UseTable[ormUniqLax]", err)
  273. m, found := coreTbl(db, "uniq").IndexMeta().FindIndex("idx_uniq_Name")
  274. check("前提: idx_uniq_Name 已存在且非唯一", found && !m.Unique(), fmt.Sprintf("found=%v", found))
  275. fatal("Commit", db.Commit())
  276. // 换成声明了 unique 的模型重开: 约束无法生效, 必须当场报错
  277. _, err = DB.UseTable[ormUniq](db, "uniq")
  278. check("模型声明 unique 而库里索引不唯一 → 重开报错", err != nil, "竟然静默沿用了非唯一索引")
  279. }