package gormvrtx // 无 size string 字段的 Blob 链式列归宿: 动态列单值上限 16360 字节, 不定长大文本 // 运行时会撞墙; Blob 列行内 8B 引用 + 链页 (~4GB), 驱动 SELECT 物化后 GORM 照常 // 扫进 string。这里钉住三件事: AutoMigrate 的列类型、60KB 值的 Create/First 往返、 // 显式 type:text 的同归宿与带索引标签时的友好报错。 import ( "strings" "testing" "gorm.io/gorm" "git.ycl.pub/yl32940/TsDB2/DB/core/types" ) // Article 正文模型: Content 无 size → AutoMigrate 应落成 Blob 链式列。 type Article struct { ID uint `gorm:"primaryKey"` Title string `gorm:"size:64;not null"` Content string // 无 size: 不定长大文本 } func (Article) TableName() string { return "articles" } // Memo 显式 type:text —— 与无 size string 同一归宿 (Blob 链式列)。 type Memo struct { ID uint `gorm:"primaryKey"` Content string `gorm:"type:text"` } func (Memo) TableName() string { return "memos" } // TestStringNoSizeAutoMigrateToBlob 无 size string 字段 (含显式 type:text) // AutoMigrate 后必须是 Blob 列; DataTypeOf 对两类字段都报 BLOB; 带 size 的照旧定长。 func TestStringNoSizeAutoMigrateToBlob(t *testing.T) { db := openTest(t) if err := db.AutoMigrate(&Article{}, &Memo{}); err != nil { t.Fatalf("AutoMigrate: %v", err) } d := db.Dialector.(*Dialector) fieldType := func(table, col string) types.FieldType { t.Helper() tbl, ok := d.db.FindTable(table) if !ok { t.Fatalf("表 %s 不存在", table) } for i := range tbl.Field { f := &tbl.Field[i] if !f.IsDropped() && strings.EqualFold(string(f.FieldName), col) { return f.FieldType } } t.Fatalf("表 %s 没有列 %s", table, col) return 0 } if got := fieldType("articles", "content"); got != types.FieldType_Blob { t.Fatalf("无 size 的 content 列应落成 FieldType_Blob, 实际 %v", got) } if got := fieldType("articles", "title"); got != types.FieldType_String { t.Fatalf("size:64 的 title 列应落成 FieldType_String, 实际 %v", got) } if got := fieldType("memos", "content"); got != types.FieldType_Blob { t.Fatalf("显式 type:text 的 content 列应落成 FieldType_Blob, 实际 %v", got) } // Migrator 自省 (ColumnTypes) 与 DataTypeOf 同口径对外。 cols, err := db.Migrator().ColumnTypes(&Article{}) if err != nil { t.Fatalf("ColumnTypes: %v", err) } for _, c := range cols { switch c.Name() { case "content": if c.DatabaseTypeName() != "blob" { t.Fatalf("content 的 DatabaseTypeName 应为 blob, 实际 %q", c.DatabaseTypeName()) } case "title": if c.DatabaseTypeName() != "string" { t.Fatalf("title 的 DatabaseTypeName 应为 string, 实际 %q", c.DatabaseTypeName()) } if l, ok := c.Length(); !ok || l != 64 { t.Fatalf("title 的长度应为 64, 实际 %d (%v)", l, ok) } } } stmt := &gorm.Statement{DB: db} if err := stmt.Parse(&Article{}); err != nil { t.Fatalf("Parse: %v", err) } if got := db.Migrator().FullDataTypeOf(stmt.Schema.FieldsByDBName["content"]).SQL; got != "BLOB" { t.Fatalf("DataTypeOf(content) = %q, 想要 BLOB", got) } if err := stmt.Parse(&Memo{}); err != nil { t.Fatalf("Parse: %v", err) } if got := db.Migrator().FullDataTypeOf(stmt.Schema.FieldsByDBName["content"]).SQL; !strings.HasPrefix(got, "BLOB") { t.Fatalf("DataTypeOf(type:text content) = %q, 想要 BLOB 开头", got) } } // TestBlobContentRoundTrip 60KB 正文 (超动态列单值上限 16360) Create 进去、 // First 读回来逐字节一致 —— 链式写入与 SELECT 物化全链路。 func TestBlobContentRoundTrip(t *testing.T) { db := openTest(t) if err := db.AutoMigrate(&Article{}); err != nil { t.Fatalf("AutoMigrate: %v", err) } content := strings.Repeat("正文字段往返验证-0123456789abcdef", 2200) // >16360 字节 a := &Article{Title: "deep-dive", Content: content} if err := db.Create(a).Error; err != nil { t.Fatalf("Create (60KB blob): %v", err) } if a.ID == 0 { t.Fatal("主键应回填") } var got Article if err := db.First(&got, a.ID).Error; err != nil { t.Fatalf("First: %v", err) } if got.Content != content { t.Fatalf("60KB 正文往返不一致: got %d 字节, want %d 字节", len(got.Content), len(content)) } if got.Title != "deep-dive" { t.Fatalf("title = %q", got.Title) } // 按定长列过滤 + 只取部分行: blob 列不影响其它列的查询。 var hits []Article if err := db.Where("title = ?", "deep-dive").Limit(1).Find(&hits).Error; err != nil { t.Fatalf("Where: %v", err) } if len(hits) != 1 || hits[0].Content != content { t.Fatalf("Where 命中行的正文不一致 (%d 行)", len(hits)) } } // ArticleBad 无 size 还带索引标签: 建表阶段必须给友好错误, 而不是引擎的裸拒绝。 type ArticleBad struct { ID uint `gorm:"primaryKey"` Content string `gorm:"index"` // 无 size → Blob 列, 带索引意图 } func (ArticleBad) TableName() string { return "article_bads" } func TestBlobColumnIndexIntentError(t *testing.T) { db := openTest(t) err := db.AutoMigrate(&ArticleBad{}) if err == nil { t.Fatal("无 size 字符串字段带索引标签, AutoMigrate 应报友好错误") } for _, w := range []string{"Blob 链式列", "size:1..2048"} { if !strings.Contains(err.Error(), w) { t.Fatalf("错误里缺少 %q: %v", w, err) } } }