blob_text_test.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package gormvrtx
  2. // 无 size string 字段的 Blob 链式列归宿: 动态列单值上限 16360 字节, 不定长大文本
  3. // 运行时会撞墙; Blob 列行内 8B 引用 + 链页 (~4GB), 驱动 SELECT 物化后 GORM 照常
  4. // 扫进 string。这里钉住三件事: AutoMigrate 的列类型、60KB 值的 Create/First 往返、
  5. // 显式 type:text 的同归宿与带索引标签时的友好报错。
  6. import (
  7. "strings"
  8. "testing"
  9. "gorm.io/gorm"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  11. )
  12. // Article 正文模型: Content 无 size → AutoMigrate 应落成 Blob 链式列。
  13. type Article struct {
  14. ID uint `gorm:"primaryKey"`
  15. Title string `gorm:"size:64;not null"`
  16. Content string // 无 size: 不定长大文本
  17. }
  18. func (Article) TableName() string { return "articles" }
  19. // Memo 显式 type:text —— 与无 size string 同一归宿 (Blob 链式列)。
  20. type Memo struct {
  21. ID uint `gorm:"primaryKey"`
  22. Content string `gorm:"type:text"`
  23. }
  24. func (Memo) TableName() string { return "memos" }
  25. // TestStringNoSizeAutoMigrateToBlob 无 size string 字段 (含显式 type:text)
  26. // AutoMigrate 后必须是 Blob 列; DataTypeOf 对两类字段都报 BLOB; 带 size 的照旧定长。
  27. func TestStringNoSizeAutoMigrateToBlob(t *testing.T) {
  28. db := openTest(t)
  29. if err := db.AutoMigrate(&Article{}, &Memo{}); err != nil {
  30. t.Fatalf("AutoMigrate: %v", err)
  31. }
  32. d := db.Dialector.(*Dialector)
  33. fieldType := func(table, col string) types.FieldType {
  34. t.Helper()
  35. tbl, ok := d.db.FindTable(table)
  36. if !ok {
  37. t.Fatalf("表 %s 不存在", table)
  38. }
  39. for i := range tbl.Field {
  40. f := &tbl.Field[i]
  41. if !f.IsDropped() && strings.EqualFold(string(f.FieldName), col) {
  42. return f.FieldType
  43. }
  44. }
  45. t.Fatalf("表 %s 没有列 %s", table, col)
  46. return 0
  47. }
  48. if got := fieldType("articles", "content"); got != types.FieldType_Blob {
  49. t.Fatalf("无 size 的 content 列应落成 FieldType_Blob, 实际 %v", got)
  50. }
  51. if got := fieldType("articles", "title"); got != types.FieldType_String {
  52. t.Fatalf("size:64 的 title 列应落成 FieldType_String, 实际 %v", got)
  53. }
  54. if got := fieldType("memos", "content"); got != types.FieldType_Blob {
  55. t.Fatalf("显式 type:text 的 content 列应落成 FieldType_Blob, 实际 %v", got)
  56. }
  57. // Migrator 自省 (ColumnTypes) 与 DataTypeOf 同口径对外。
  58. cols, err := db.Migrator().ColumnTypes(&Article{})
  59. if err != nil {
  60. t.Fatalf("ColumnTypes: %v", err)
  61. }
  62. for _, c := range cols {
  63. switch c.Name() {
  64. case "content":
  65. if c.DatabaseTypeName() != "blob" {
  66. t.Fatalf("content 的 DatabaseTypeName 应为 blob, 实际 %q", c.DatabaseTypeName())
  67. }
  68. case "title":
  69. if c.DatabaseTypeName() != "string" {
  70. t.Fatalf("title 的 DatabaseTypeName 应为 string, 实际 %q", c.DatabaseTypeName())
  71. }
  72. if l, ok := c.Length(); !ok || l != 64 {
  73. t.Fatalf("title 的长度应为 64, 实际 %d (%v)", l, ok)
  74. }
  75. }
  76. }
  77. stmt := &gorm.Statement{DB: db}
  78. if err := stmt.Parse(&Article{}); err != nil {
  79. t.Fatalf("Parse: %v", err)
  80. }
  81. if got := db.Migrator().FullDataTypeOf(stmt.Schema.FieldsByDBName["content"]).SQL; got != "BLOB" {
  82. t.Fatalf("DataTypeOf(content) = %q, 想要 BLOB", got)
  83. }
  84. if err := stmt.Parse(&Memo{}); err != nil {
  85. t.Fatalf("Parse: %v", err)
  86. }
  87. if got := db.Migrator().FullDataTypeOf(stmt.Schema.FieldsByDBName["content"]).SQL; !strings.HasPrefix(got, "BLOB") {
  88. t.Fatalf("DataTypeOf(type:text content) = %q, 想要 BLOB 开头", got)
  89. }
  90. }
  91. // TestBlobContentRoundTrip 60KB 正文 (超动态列单值上限 16360) Create 进去、
  92. // First 读回来逐字节一致 —— 链式写入与 SELECT 物化全链路。
  93. func TestBlobContentRoundTrip(t *testing.T) {
  94. db := openTest(t)
  95. if err := db.AutoMigrate(&Article{}); err != nil {
  96. t.Fatalf("AutoMigrate: %v", err)
  97. }
  98. content := strings.Repeat("正文字段往返验证-0123456789abcdef", 2200) // >16360 字节
  99. a := &Article{Title: "deep-dive", Content: content}
  100. if err := db.Create(a).Error; err != nil {
  101. t.Fatalf("Create (60KB blob): %v", err)
  102. }
  103. if a.ID == 0 {
  104. t.Fatal("主键应回填")
  105. }
  106. var got Article
  107. if err := db.First(&got, a.ID).Error; err != nil {
  108. t.Fatalf("First: %v", err)
  109. }
  110. if got.Content != content {
  111. t.Fatalf("60KB 正文往返不一致: got %d 字节, want %d 字节", len(got.Content), len(content))
  112. }
  113. if got.Title != "deep-dive" {
  114. t.Fatalf("title = %q", got.Title)
  115. }
  116. // 按定长列过滤 + 只取部分行: blob 列不影响其它列的查询。
  117. var hits []Article
  118. if err := db.Where("title = ?", "deep-dive").Limit(1).Find(&hits).Error; err != nil {
  119. t.Fatalf("Where: %v", err)
  120. }
  121. if len(hits) != 1 || hits[0].Content != content {
  122. t.Fatalf("Where 命中行的正文不一致 (%d 行)", len(hits))
  123. }
  124. }
  125. // ArticleBad 无 size 还带索引标签: 建表阶段必须给友好错误, 而不是引擎的裸拒绝。
  126. type ArticleBad struct {
  127. ID uint `gorm:"primaryKey"`
  128. Content string `gorm:"index"` // 无 size → Blob 列, 带索引意图
  129. }
  130. func (ArticleBad) TableName() string { return "article_bads" }
  131. func TestBlobColumnIndexIntentError(t *testing.T) {
  132. db := openTest(t)
  133. err := db.AutoMigrate(&ArticleBad{})
  134. if err == nil {
  135. t.Fatal("无 size 字符串字段带索引标签, AutoMigrate 应报友好错误")
  136. }
  137. for _, w := range []string{"Blob 链式列", "size:1..2048"} {
  138. if !strings.Contains(err.Error(), w) {
  139. t.Fatalf("错误里缺少 %q: %v", w, err)
  140. }
  141. }
  142. }