package gormvrtx // Migrator: GORM schema 自省不经 SQL, 直接调 TsDB2 引擎 API。 // 核心方法 (建表/删表/查表/增删列/查列/建删查索引/列类型) 为真实现; // 引擎无对应能力的 (改列类型/重命名/约束/视图) 返回明确的"不支持"错误, // 而不是静默做错。 import ( "errors" "fmt" "reflect" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/schema" "git.ycl.pub/yl32940/TsDB2/DB/core" "git.ycl.pub/yl32940/TsDB2/DB/core/page" "git.ycl.pub/yl32940/TsDB2/DB/core/types" ) // Migrator 实现 gorm.Migrator。 type Migrator struct { db *gorm.DB eng *core.DB dsn string dialector *Dialector } var _ gorm.Migrator = Migrator{} // errNotSupported 构造"引擎不支持"错误。 func errNotSupported(op string) error { return fmt.Errorf("vrtx: 不支持 %s (引擎无对应能力)", op) } // ============================================================================= // 模型/表解析助手 (对齐 GORM 官方 migrator 的拿表名姿势) // ============================================================================= // stmtFor 解析 dst 为 *gorm.Statement: 字符串视为表名, 其余经 schema 解析 // (NamingStrategy / Tabler 接口都生效), stmt.Table 即最终表名。 func (m Migrator) stmtFor(value interface{}) (*gorm.Statement, error) { stmt := &gorm.Statement{DB: m.db} if m.db != nil && m.db.Statement != nil { stmt.Table = m.db.Statement.Table stmt.TableExpr = m.db.Statement.TableExpr } if table, ok := value.(string); ok { stmt.Table = table } else if err := stmt.ParseWithSpecialTableName(value, stmt.Table); err != nil { return nil, err } if stmt.Table == "" { return nil, errors.New("vrtx: 无法确定表名") } return stmt, nil } // findTable 按表名取引擎表句柄。 func (m Migrator) findTable(name string) (*core.Table, error) { tbl, ok := m.eng.FindTable(name) if !ok { return nil, fmt.Errorf("%w: %q", core.ErrTableNotFound, name) } return tbl, nil } // tableFieldIndex 列名 → 引擎字段主注册表下标 (含已删除列的位置; 索引 FieldIDs // 即按这个下标引用, 与驱动的 fieldIndex 约定一致), 大小写不敏感。 func tableFieldIndex(tbl *core.Table, name string) int { for i := range tbl.Field { if !tbl.Field[i].IsDropped() && strings.EqualFold(string(tbl.Field[i].FieldName), name) { return i } } return -1 } // fieldInfoOf GORM 字段 → 引擎列定义。显式 gorm:"type:..." 标签优先于 Go 类型默认 // 映射 (报告 #10 的逃生门: string 字段默认无 size 会落成 Blob 链式列, 带索引时引擎 // 拒绝建索引 —— 用 type:varchar(500)/type:bigint 精确声明列类型/宽度即可绕过)。 func (m Migrator) fieldInfoOf(f *schema.Field) (types.FieldInfo, error) { fi := types.FieldInfo{FieldName: []byte(f.DBName)} // 可空: NotNull==false 且非主键 → 允许 NULL。主键列是行定位键, 绝不可空。 if !f.NotNull && !f.PrimaryKey { fi.Flag |= types.FlagNullable } if tag, ok := f.TagSettings["TYPE"]; ok { if name, size, ok := explicitTypeMapping(tag); ok { switch name { case "BIGINT", "INT": // 显式整型声明: 符号随 Go 类型 (uint64 超出 int64 可表数域, 不能错配)。 if f.DataType == schema.Uint { fi.FieldType = types.FieldType_Uint64 } else { fi.FieldType = types.FieldType_Int64 } case "DOUBLE": fi.FieldType = types.FieldType_Float64 case "BOOLEAN": fi.FieldType = types.FieldType_Bool case "DATETIME": fi.FieldType = types.FieldType_DateTime case "TEXT": // 显式 type:text 与无 size string 同一归宿: Blob 链式列 (权衡见 // stringFieldInfo) —— 动态列单值上限 16360, 不定长大文本运行时会撞墙。 fi.FieldType = types.FieldType_Blob case "VARCHAR": if size <= 0 { // 显式 type:varchar 却没给宽度: 与 DataTypeOf 的注释同一口径, // 按动态列处理 (无长度语义的文本), 不落到下面的 Blob 默认。 fi.FieldType = types.FieldType_Dynamicbyte return fi, nil } return stringFieldInfo(fi, f.DBName, size) } return fi, nil } // 未知 type: 落回默认映射 (与 DataTypeOf 的注释一致)。 } switch f.DataType { case schema.Bool: fi.FieldType = types.FieldType_Bool case schema.Int: fi.FieldType = types.FieldType_Int64 case schema.Uint: fi.FieldType = types.FieldType_Uint64 case schema.Float: fi.FieldType = types.FieldType_Float64 case schema.String: return stringFieldInfo(fi, f.DBName, f.Size) case schema.Time: fi.FieldType = types.FieldType_DateTime case schema.Bytes: fi.FieldType = types.FieldType_Blob default: // struct / JSON 等: 文本化存动态列 fi.FieldType = types.FieldType_Dynamicbyte } return fi, nil } // stringFieldInfo string 字段按声明宽度落成引擎列: 无宽度 → Blob 链式列; 声明超行内 // 定长上限 (types.MaxInlineColumnWidth = 2048) → 动态列; 其余定长 string 并写宽度码。 // 宽度声明兑现不了 (引擎档位限制) 返回错误。 // // 无宽度落 Blob 而不是旧行为的动态列: 动态列单值上限 types.MaxStringSize (16360 字节, // 内容整块落一个堆页), 不定长大文本写入超限即报错; Blob 列行内 8B BlobRef + 链页, // 单值约 4GB —— 这正是 migrate 对超宽 TEXT 源列的自动归宿。读回时驱动把链内容物化 // 成 []byte, GORM 可扫进 string。代价: 该列失去 LIKE/索引能力 (行内只有引用)。 func stringFieldInfo(fi types.FieldInfo, dbName string, size int) (types.FieldInfo, error) { if size < 0 { size = 0 } switch { case size == 0: fi.FieldType = types.FieldType_Blob // 不定长大文本: 链式存储 case size > types.MaxInlineColumnWidth: fi.FieldType = types.FieldType_Dynamicbyte // 声明超行内定长上限 → 动态列 default: if err := types.CheckDeclaredStringSize(size); err != nil { return fi, fmt.Errorf("列 %q: %w", dbName, err) } fi.FieldType = types.FieldType_String fi.Flag |= types.WidthCodeFor(size) } return fi, nil } // isIntDataType 整型 (含 uint) GORM 字段 —— 主键索引只接受整型定位键。 func isIntDataType(f *schema.Field) bool { return f.DataType == schema.Int || f.DataType == schema.Uint } // pkIndexName 行存储主键索引名 (与驱动 CREATE TABLE ... PRIMARY KEY 路径一致)。 func pkIndexName(table string) string { return "pk_" + table } // uniqueIndexName 单列唯一索引名 (field.Unique 的承载)。 func uniqueIndexName(table, col string) string { return "uk_" + table + "_" + col } // hasIndexIntent 字段是否声明了索引意图: index / uniqueIndex tag 或字段级 unique。 // (无值标签经 GORM ParseTagSetting 会存成 "INDEX"/"UNIQUEINDEX" 字面值, 判空即覆盖 // `gorm:"index"` 与 `gorm:"index:idx_x"` 两种写法。) func hasIndexIntent(f *schema.Field) bool { return f.Unique || f.TagSettings["INDEX"] != "" || f.TagSettings["UNIQUEINDEX"] != "" } // dynamicColumnErr 报告 #10 的友好错误: 字段带索引标签却落成不可索引的列时, 在建表 // 阶段就给出可操作的提示, 而不是等建索引时被引擎顶回来。不可索引的列有两种: // 动态列 (行内堆引用) 与 Blob 链式列 (行内 8B 链引用) —— 无 size 的 string 字段 // 现在是后者的默认归宿。字段不带索引意图、或落成可索引类型时返回 nil。 func (m Migrator) dynamicColumnErr(f *schema.Field) error { if !hasIndexIntent(f) { return nil } fi, err := m.fieldInfoOf(f) if err != nil { return err } switch fi.FieldType { case types.FieldType_Dynamicbyte: return fmt.Errorf("字段 %s 带索引标签但会落成动态列(size>2048),无法建索引:请给该字段加 gorm:\"size:1..2048\"", f.Name) case types.FieldType_Blob: return fmt.Errorf("字段 %s 带索引标签但会落成 Blob 链式列(无 size 字符串字段的默认归宿),无法建索引:需要可搜索的文本请给该字段加 gorm:\"size:1..2048\" 走定长档", f.Name) } return nil } // indexTypeFor 单列 → B+ 树二级索引; 多列 → 复合索引。 func indexTypeFor(fieldCnt int) page.IndexType { if fieldCnt > 1 { return page.IndexTypeCompose } return page.IndexTypeBTree } // createColumnIndex 按列下标在引擎表上建索引 (unique 透传)。 func createColumnIndex(tbl *core.Table, name string, fieldIDs []byte, unique bool) error { if len(fieldIDs) == 0 { return fmt.Errorf("vrtx: 索引 %q 没有分量列", name) } _, err := tbl.CreateIndex(name, indexTypeFor(len(fieldIDs)), fieldIDs, unique) return err } // ============================================================================= // AutoMigrate / Database // ============================================================================= // AutoMigrate 首次建表 (含主键/唯一/普通索引), 已有表则补缺失的列与索引。 // 引擎 AddField 不支持改列类型, 已存在列的类型微调一律放过 (宽松语义)。 func (m Migrator) AutoMigrate(values ...interface{}) error { for _, value := range values { stmt, err := m.stmtFor(value) if err != nil { return err } if !m.HasTable(stmt.Table) { if err := m.CreateTable(value); err != nil { return err } continue } // 表已存在: 补列 (DBNames 排除 has-many 等关联字段 —— 它们没有列) for _, dbName := range stmt.Schema.DBNames { if !m.HasColumn(value, dbName) { if err := m.AddColumn(value, dbName); err != nil { return err } } } // 补索引: tag 声明的 (uniqueIndex/index) + 字段级 unique for _, idx := range stmt.Schema.ParseIndexes() { if m.HasIndex(value, idx.Name) { continue } if err := m.CreateIndex(value, idx.Name); err != nil { return err } } for _, f := range stmt.Schema.Fields { if !f.Unique || f.PrimaryKey { continue } // 报告 #10: 唯一约束落到动态列上同样在建索引前给友好错误。 if err := m.dynamicColumnErr(f); err != nil { return fmt.Errorf("vrtx: 迁移 %q: %w", stmt.Table, err) } name := uniqueIndexName(stmt.Table, f.DBName) if m.HasIndex(value, name) { continue } tbl, err := m.findTable(stmt.Table) if err != nil { return err } if err := createColumnIndex(tbl, name, []byte{byte(tableFieldIndex(tbl, f.DBName))}, true); err != nil { return err } } } return nil } // CurrentDatabase 返回 DSN (引擎无 catalog 概念, 库=文件)。 func (m Migrator) CurrentDatabase() string { return m.dsn } // FullDataTypeOf 纯类型名 (主键等约束在建表/建索引时表达, 不拼进类型)。 func (m Migrator) FullDataTypeOf(field *schema.Field) clause.Expr { return clause.Expr{SQL: m.dialector.DataTypeOf(field)} } // GetTypeAliases 引擎无类型别名体系。 func (m Migrator) GetTypeAliases(databaseTypeName string) []string { return nil } // ============================================================================= // Tables // ============================================================================= // CreateTable 对每个模型: 字段 → 引擎 CreateTable; 整型主键字段 → 行存储主键索引; // field.Unique → 唯一索引; uniqueIndex/index tag → 对应索引 (全部经引擎 API)。 // 主键索引恰好建一次: 建前先查 GetRowStoreIndex (引擎 CreateTable 可能已自动建行存储, // 报告 #8 —— 不查会撞 ErrRowStoreExists "表上已存在行存储索引"); 行存储全表唯一, 复合 // 主键只取声明的第一个整型字段; 唯一/普通索引循环对主键字段一律跳过。 func (m Migrator) CreateTable(values ...interface{}) error { for _, value := range values { stmt, err := m.stmtFor(value) if err != nil { return err } if stmt.Schema == nil { return errors.New("vrtx: CreateTable 需要模型, 得不到 schema") } if m.HasTable(stmt.Table) { continue // 幂等: 已存在则跳过 (对齐 gorm 各方言的 IF NOT EXISTS 语义) } // 建表阶段先核一遍索引字段的可索引性 (报告 #10): 带 index/uniqueIndex/unique // 却落成动态列的字段, 在表创建前就给出可操作的错误, 而不是建索引时才被引擎的 // "动态列不能作为索引键" 顶回来。 for _, dbName := range stmt.Schema.DBNames { f := stmt.Schema.FieldsByDBName[dbName] if err := m.dynamicColumnErr(f); err != nil { return fmt.Errorf("vrtx: 建表 %q: %w", stmt.Table, err) } } infos := make([]types.FieldInfo, 0, len(stmt.Schema.DBNames)) for _, dbName := range stmt.Schema.DBNames { fi, err := m.fieldInfoOf(stmt.Schema.FieldsByDBName[dbName]) if err != nil { return fmt.Errorf("vrtx: 建表 %q: %w", stmt.Table, err) } infos = append(infos, fi) } tbl, err := m.eng.CreateTable(stmt.Table, infos) if err != nil { return fmt.Errorf("vrtx: 建表 %q: %w", stmt.Table, err) } // 整型主键 → 行存储主键索引 (引擎表的行定位键)。 if _, hasRowStore := tbl.IndexMeta().GetRowStoreIndex(); !hasRowStore { for _, f := range stmt.Schema.Fields { if !f.PrimaryKey || !isIntDataType(f) { continue } idx := tableFieldIndex(tbl, f.DBName) if idx < 0 { return fmt.Errorf("vrtx: 建表 %q: 找不到主键列 %q", stmt.Table, f.DBName) } if _, err := tbl.CreateIndex(pkIndexName(stmt.Table), page.IndexTypePrimary, []byte{byte(idx)}); err != nil { return fmt.Errorf("vrtx: 建表 %q 主键索引: %w", stmt.Table, err) } break // 行存储全表唯一: 复合主键也只建行定位键这一份 } } // 字段级 unique (主键已由行存储承载, 跳过) for _, f := range stmt.Schema.Fields { if !f.Unique || f.PrimaryKey || f.DBName == "" { continue } idx := tableFieldIndex(tbl, f.DBName) if idx < 0 { return fmt.Errorf("vrtx: 建表 %q: 找不到列 %q", stmt.Table, f.DBName) } if err := createColumnIndex(tbl, uniqueIndexName(stmt.Table, f.DBName), []byte{byte(idx)}, true); err != nil { return fmt.Errorf("vrtx: 建表 %q 唯一索引: %w", stmt.Table, err) } } // tag 声明的索引 (uniqueIndex / index, 含复合) for _, idx := range stmt.Schema.ParseIndexes() { // 分量全是主键列的索引定义直接跳过: 主键唯一性已由行存储承载, 对主键列再建 // 二级索引纯属冗余 (primaryKey 与 uniqueIndex 标在同一字段时防止重复建)。 allPK := len(idx.Fields) > 0 for _, opt := range idx.Fields { if opt.Field == nil || !opt.Field.PrimaryKey { allPK = false break } } if allPK { continue } fieldIDs := make([]byte, 0, len(idx.Fields)) for _, opt := range idx.Fields { if opt.Field == nil { return fmt.Errorf("vrtx: 建表 %q: 索引 %q 含表达式分量, 引擎不支持", stmt.Table, idx.Name) } // 复合索引里的无标签分量也要核可索引性 (它进了索引就该受索引约束)。 if err := m.dynamicColumnErr(opt.Field); err != nil { return fmt.Errorf("vrtx: 建表 %q: %w", stmt.Table, err) } fi := tableFieldIndex(tbl, opt.Field.DBName) if fi < 0 { return fmt.Errorf("vrtx: 建表 %q: 索引 %q 引用了不存在的列 %q", stmt.Table, idx.Name, opt.Field.DBName) } fieldIDs = append(fieldIDs, byte(fi)) } if err := createColumnIndex(tbl, idx.Name, fieldIDs, idx.Class == "UNIQUE"); err != nil { return fmt.Errorf("vrtx: 建表 %q 索引 %q: %w", stmt.Table, idx.Name, err) } } } return nil } // DropTable 逐表删除。 func (m Migrator) DropTable(values ...interface{}) error { for _, value := range values { stmt, err := m.stmtFor(value) if err != nil { return err } if err := m.eng.DropTable(stmt.Table); err != nil { return fmt.Errorf("vrtx: 删表 %q: %w", stmt.Table, err) } } return nil } // HasTable 表存在性。 func (m Migrator) HasTable(value interface{}) bool { stmt, err := m.stmtFor(value) if err != nil { return false } _, ok := m.eng.FindTable(stmt.Table) return ok } // RenameTable 调 core.DB.RenameTable (DDL 隐式提交)。 // oldName/newName 支持 string 或实现 schema.Tabler 的模型。 func (m Migrator) RenameTable(oldName, newName interface{}) error { oldTable := tableNameOf(oldName) if oldTable == "" { return fmt.Errorf("vrtx: RenameTable 无法解析 oldName") } newTable := tableNameOf(newName) if newTable == "" { return fmt.Errorf("vrtx: RenameTable 无法解析 newName") } if err := m.eng.RenameTable(oldTable, newTable); err != nil { return fmt.Errorf("vrtx: 重命名表 %q → %q: %w", oldTable, newTable, err) } return nil } // tableNameOf 把 GORM RenameTable 参数 (string / schema.Tabler) 解析成表名字符串。 func tableNameOf(v interface{}) string { switch x := v.(type) { case string: return x case schema.Tabler: return x.TableName() } return "" } // GetTables 列出全部表名。 func (m Migrator) GetTables() ([]string, error) { metas := m.eng.GetAllTables() names := make([]string, 0, len(metas)) for _, meta := range metas { names = append(names, meta.Name) } return names, nil } // TableType 引擎无表类型体系 (无临时表/外部表之分)。 func (m Migrator) TableType(dst interface{}) (gorm.TableType, error) { return nil, errNotSupported("TableType") } // ============================================================================= // Columns // ============================================================================= // AddColumn 往已有表加一列 (dst=模型, field=列名/DBName)。 func (m Migrator) AddColumn(value interface{}, field string) error { stmt, err := m.stmtFor(value) if err != nil { return err } tbl, err := m.findTable(stmt.Table) if err != nil { return err } f := stmt.Schema.LookUpField(field) if f == nil { return fmt.Errorf("vrtx: 模型上没有列 %q", field) } fi, err := m.fieldInfoOf(f) if err != nil { return fmt.Errorf("vrtx: 加列 %q.%q: %w", stmt.Table, field, err) } if err := tbl.AddField(fi); err != nil { return fmt.Errorf("vrtx: 加列 %q.%q: %w", stmt.Table, field, err) } return nil } // DropColumn 软删一列 (引擎 master registry 保留, 旧行不受影响)。 func (m Migrator) DropColumn(value interface{}, field string) error { stmt, err := m.stmtFor(value) if err != nil { return err } tbl, err := m.findTable(stmt.Table) if err != nil { return err } if err := tbl.DropField(field); err != nil { return fmt.Errorf("vrtx: 删列 %q.%q: %w", stmt.Table, field, err) } return nil } // AlterColumn 引擎 AddField 不支持改列类型/宽度, 明确报错。 func (m Migrator) AlterColumn(value interface{}, field string) error { return errNotSupported("AlterColumn (引擎不支持修改列定义)") } // MigrateColumn 宽松实现: 列已存在即视为一致 (引擎不支持类型微调, 报错没有意义)。 func (m Migrator) MigrateColumn(value interface{}, field *schema.Field, columnType gorm.ColumnType) error { return nil } // MigrateColumnUnique 同上: 唯一性由唯一索引承载, 列级核对一律放过。 func (m Migrator) MigrateColumnUnique(value interface{}, field *schema.Field, columnType gorm.ColumnType) error { return nil } // HasColumn 遍历引擎字段按名匹配 (大小写不敏感)。 func (m Migrator) HasColumn(value interface{}, field string) bool { stmt, err := m.stmtFor(value) if err != nil { return false } tbl, err := m.findTable(stmt.Table) if err != nil { return false } return tableFieldIndex(tbl, field) >= 0 } // RenameColumn 调 core.DB.RenameColumn (DDL 隐式提交)。 func (m Migrator) RenameColumn(value interface{}, oldName, field string) error { stmt, err := m.stmtFor(value) if err != nil { return err } if err := m.eng.RenameColumn(stmt.Table, oldName, field); err != nil { return fmt.Errorf("vrtx: 重命名列 %q.%q → %q: %w", stmt.Table, oldName, field, err) } return nil } // ColumnTypes 从引擎元数据构造列类型列表 (供高级用法)。 func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) { stmt, err := m.stmtFor(value) if err != nil { return nil, err } tbl, err := m.findTable(stmt.Table) if err != nil { return nil, err } rowStore, hasRowStore := tbl.IndexMeta().GetRowStoreIndex() cols := make([]gorm.ColumnType, 0, len(tbl.Field)) for i := range tbl.Field { fi := &tbl.Field[i] if fi.IsDropped() { continue } param := fi.Flag & types.FlagScaleMask ct := &columnType{ name: string(fi.FieldName), dbTypeName: types.TypeNameOf(fi.FieldType), nullable: fi.IsNullable(), scanType: scanTypeOf(fi.FieldType), } if fi.FieldType == types.FieldType_String { ct.length = int64(types.FieldSize(fi.FieldType, param)) ct.hasLength = true } if fi.FieldType == types.FieldType_Decimal { ct.scale = int64(param) ct.hasScale = true } if hasRowStore { for _, fid := range rowStore.Fields() { if int(fid) == i { ct.primaryKey = true break } } } cols = append(cols, ct) } return cols, nil } // columnType 实现 gorm.ColumnType 接口 (一套小结构体, 值全部来自引擎元数据)。 type columnType struct { name string dbTypeName string nullable bool primaryKey bool unique bool length int64 hasLength bool scale int64 hasScale bool scanType reflect.Type } func (c *columnType) Name() string { return c.name } func (c *columnType) DatabaseTypeName() string { return c.dbTypeName } func (c *columnType) ColumnType() (string, bool) { if c.hasLength { return fmt.Sprintf("%s(%d)", c.dbTypeName, c.length), true } if c.hasScale { return fmt.Sprintf("%s(38,%d)", c.dbTypeName, c.scale), true } return c.dbTypeName, true } func (c *columnType) PrimaryKey() (bool, bool) { return c.primaryKey, true } func (c *columnType) AutoIncrement() (bool, bool) { return c.primaryKey, true } // 主键镜像列由引擎自增 func (c *columnType) Length() (int64, bool) { return c.length, c.hasLength } func (c *columnType) DecimalSize() (int64, int64, bool) { return 38, c.scale, c.hasScale } func (c *columnType) Nullable() (bool, bool) { return c.nullable, true } func (c *columnType) Unique() (bool, bool) { return c.unique, c.unique } func (c *columnType) ScanType() reflect.Type { return c.scanType } func (c *columnType) Comment() (string, bool) { return "", false // 引擎无列注释 (Comment 是库级介绍区) } func (c *columnType) DefaultValue() (string, bool) { return "", false } // scanTypeOf 引擎列类型 → Go 扫描类型。 func scanTypeOf(ft types.FieldType) reflect.Type { switch ft { case types.FieldType_Bool: return reflect.TypeOf(false) case types.FieldType_Int8, types.FieldType_Int16, types.FieldType_Int32, types.FieldType_Int64, types.FieldType_Uint8, types.FieldType_Uint16, types.FieldType_Uint32, types.FieldType_Uint64, types.FieldType_Year: return reflect.TypeOf(int64(0)) case types.FieldType_Float32, types.FieldType_Float64: return reflect.TypeOf(float64(0)) case types.FieldType_DateTime: return reflect.TypeOf(time.Time{}) case types.FieldType_Blob, types.FieldType_Dynamicbyte: return reflect.TypeOf([]byte(nil)) default: // String / Decimal / UUID / Int128 等文本化输出 return reflect.TypeOf("") } } // ============================================================================= // Views —— 引擎无视图 // ============================================================================= func (m Migrator) CreateView(name string, option gorm.ViewOption) error { return errNotSupported("CreateView") } func (m Migrator) DropView(name string) error { return errNotSupported("DropView") } // ============================================================================= // Constraints —— 引擎无约束体系 (唯一性由唯一索引承载) // ============================================================================= func (m Migrator) CreateConstraint(value interface{}, name string) error { return errNotSupported("CreateConstraint (引擎无约束体系, 唯一性请用 uniqueIndex)") } func (m Migrator) DropConstraint(value interface{}, name string) error { return errNotSupported("DropConstraint") } func (m Migrator) HasConstraint(value interface{}, name string) bool { return false } // ============================================================================= // Indexes // ============================================================================= // CreateIndex 按模型的 uniqueIndex/index tag 解析列, 建引擎索引。 func (m Migrator) CreateIndex(value interface{}, name string) error { stmt, err := m.stmtFor(value) if err != nil { return err } if stmt.Schema == nil { return errors.New("vrtx: CreateIndex 需要模型, 得不到 schema") } idx := stmt.Schema.LookIndex(name) if idx == nil { return fmt.Errorf("vrtx: 模型 %q 上没有索引 %q", stmt.Table, name) } tbl, err := m.findTable(stmt.Table) if err != nil { return err } fieldIDs := make([]byte, 0, len(idx.Fields)) for _, opt := range idx.Fields { if opt.Field == nil { return fmt.Errorf("vrtx: 索引 %q 含表达式分量, 引擎不支持", name) } // 报告 #10: 分量落成动态列时给友好错误, 而不是引擎的 "动态列不能作为索引键"。 if err := m.dynamicColumnErr(opt.Field); err != nil { return fmt.Errorf("vrtx: 建索引 %q: %w", name, err) } fi := tableFieldIndex(tbl, opt.Field.DBName) if fi < 0 { return fmt.Errorf("vrtx: 索引 %q 引用了不存在的列 %q", name, opt.Field.DBName) } fieldIDs = append(fieldIDs, byte(fi)) } if err := createColumnIndex(tbl, idx.Name, fieldIDs, idx.Class == "UNIQUE"); err != nil { return fmt.Errorf("vrtx: 建索引 %q.%q: %w", stmt.Table, name, err) } return nil } // DropIndex 按名删索引。 func (m Migrator) DropIndex(value interface{}, name string) error { stmt, err := m.stmtFor(value) if err != nil { return err } tbl, err := m.findTable(stmt.Table) if err != nil { return err } if err := tbl.DropIndex(name); err != nil { return fmt.Errorf("vrtx: 删索引 %q.%q: %w", stmt.Table, name, err) } return nil } // HasIndex 枚举引擎索引元数据按名匹配 (大小写不敏感)。 func (m Migrator) HasIndex(value interface{}, name string) bool { stmt, err := m.stmtFor(value) if err != nil { return false } tbl, err := m.findTable(stmt.Table) if err != nil { return false } for _, meta := range tbl.IndexMeta().GetIndexes() { if strings.EqualFold(meta.Name(), name) { return true } } return false } // RenameIndex 调 core.Table.RenameIndex (DDL 隐式提交)。 func (m Migrator) RenameIndex(value interface{}, oldName, newName string) error { stmt, err := m.stmtFor(value) if err != nil { return err } tbl, err := m.findTable(stmt.Table) if err != nil { return err } if err := tbl.RenameIndex(oldName, newName); err != nil { return fmt.Errorf("vrtx: 重命名索引 %q.%q → %q: %w", stmt.Table, oldName, newName, err) } return nil } // GetIndexes 包装引擎索引元数据, 能返尽返。 func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) { stmt, err := m.stmtFor(value) if err != nil { return nil, err } tbl, err := m.findTable(stmt.Table) if err != nil { return nil, err } metas := tbl.IndexMeta().GetIndexes() out := make([]gorm.Index, 0, len(metas)) for _, meta := range metas { out = append(out, indexMeta{table: stmt.Table, meta: meta, tbl: tbl}) } return out, nil } // indexMeta 把引擎索引元数据适配成 gorm.Index。 type indexMeta struct { table string meta core.IndexMeta tbl *core.Table } func (i indexMeta) Table() string { return i.table } func (i indexMeta) Name() string { return i.meta.Name() } func (i indexMeta) Columns() []string { ids := i.meta.Fields() cols := make([]string, 0, len(ids)) for _, id := range ids { if int(id) >= 0 && int(id) < len(i.tbl.Field) { cols = append(cols, string(i.tbl.Field[id].FieldName)) } } return cols } func (i indexMeta) PrimaryKey() (bool, bool) { return i.meta.IndexType == page.IndexTypePrimary, true } func (i indexMeta) Unique() (bool, bool) { return i.meta.Unique(), true } func (i indexMeta) Option() string { return "" }