| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- 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)
- }
- }
- }
|