| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- package migrate
- import (
- "git.ycl.pub/yl32940/TsDB2/DB/core"
- "git.ycl.pub/yl32940/TsDB2/DB/core/page"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- "git.ycl.pub/yl32940/TsDB2/DB/core/util"
- "errors"
- "fmt"
- "sort"
- "strings"
- "unicode/utf8"
- )
- // VRTX 一侧。同一个对象既能当 Source 也能当 Sink (方向由调用方决定), 于是任意
- // 方向 (VRTX→SQL / SQL→VRTX / VRTX→VRTX) 都只需要这一套读写实现。
- //
- // 这里刻意走 core 层而不是 ORM (package DB): 表结构在迁移时来自源库的元数据
- // (列名 / 类型 / 行存储索引列), 运行时才知道, 根本没有一个 Go 结构体可以内嵌表基类 ——
- // 而 ORM 的入口 UseTable[T] 恰恰要求 T 内嵌 DB.BaseModel / DB.TimeSeriesModel
- // (见 DB 包的 requireBase)。core 层只认 []types.FieldInfo, 没有"先有结构体"这个前提,
- // 所以"按库里的元数据建表"只能落在 core 层。ORM 是给"写代码时就知道表结构"的场景用的。
- type VRTX struct {
- db *core.DB
- path string
- opts *Options
- }
- // OpenVRTX 打开 (不存在则新建) 一个 VRTX 库文件。
- func OpenVRTX(path string, opts *Options) (*VRTX, error) {
- openOpt := core.OpenOptions{}
- if opts != nil && opts.ForceInspect {
- openOpt.ForceInspect = true
- }
- db, err := core.OpenWithOptions(path, openOpt)
- if err != nil {
- return nil, fmt.Errorf("打开 VRTX 库 %s: %w", path, err)
- }
- return &VRTX{db: db, path: path, opts: opts.normalized()}, nil
- }
- func (v *VRTX) Kind() Kind { return KindVRTX }
- func (v *VRTX) Identity() string { return "vrtx:" + v.path }
- // DB 暴露底层句柄 (调用方可能需要调缓冲池 / 手动提交)。
- func (v *VRTX) DB() *core.DB { return v.db }
- // Commit 把当前脏页落盘。编排器每迁完一张表调一次: VRTX 的插入是攒在缓冲池里的,
- // 千万行的库若要等最后 Close 才提交, 中途崩一次就是全部白干, 内存也会一直涨。
- func (v *VRTX) Commit() error { return v.db.Commit() }
- // Close 落盘并关闭。
- func (v *VRTX) Close() error {
- // 迁移是批量写, 收尾必须显式提交一次: 否则最后一批脏页只剩防抖定时器兜底,
- // 进程立刻退出就会丢。
- if err := v.db.Commit(); err != nil {
- _ = v.db.Close()
- return err
- }
- return v.db.Close()
- }
- // Tables 列出全部表 (core 已跳过软删除的表)。
- func (v *VRTX) Tables() ([]string, error) {
- metas := v.db.GetAllTables()
- names := make([]string, 0, len(metas))
- for _, m := range metas {
- names = append(names, m.Name)
- }
- sort.Strings(names) // 稳定顺序: 迁移过程可复现、可对照
- return names, nil
- }
- // Schema 读一张表的结构。
- //
- // 列取 master registry 里未被软删除的那些 —— 注意用 master 顺序而不是
- // GetActiveFieldNames(): 后者是"当前 schema 版本"的列序, 而 master registry 的
- // 下标才是索引项里 FieldIDs 引用的那个下标, 换算列名必须用它。
- func (v *VRTX) Schema(table string) (*TableSchema, error) {
- tbl, ok := v.db.FindTable(table)
- if !ok {
- return nil, fmt.Errorf("VRTX 库 %s 里没有表 %s", v.path, table)
- }
- fields := tbl.GetFields() // master registry: 含已软删除的列 (删除标记见 types.FlagDeleted)
- s := &TableSchema{Name: table}
- for _, f := range fields {
- if f.IsDropped() {
- continue
- }
- s.Columns = append(s.Columns, Column{
- Name: util.BytesToString(f.FieldName),
- Type: f.FieldType,
- Param: f.Flag & types.FlagScaleMask, // Decimal 的列级 scale, 必须跟着列走
- // NULL / 唯一声明: 前者决定导出时写不写 NOT NULL, 后者是列级声明
- // (约束本身由索引执行, 见下面 GetIndexes 的 m.Unique())。
- Nullable: f.IsNullable(),
- Unique: f.IsUnique(),
- })
- }
- im := tbl.IndexMeta()
- pk, hasPK := im.GetRowStoreIndex()
- if !hasPK {
- return nil, fmt.Errorf("VRTX 表 %s 没有行存储索引 (主键/时序), 行数据无处安放", table)
- }
- s.PKIndexName = pk.Name()
- s.PKIndexType = pk.IndexType
- pkCols := make([]string, 0, pk.FieldCnt)
- for _, fid := range pk.Fields() {
- if int(fid) >= len(fields) {
- continue
- }
- if util.IsHighBitSet(byte(fields[fid].FieldType)) {
- continue
- }
- pkCols = append(pkCols, util.BytesToString(fields[fid].FieldName))
- }
- switch len(pkCols) {
- case 0:
- // 行存储索引的分量列全被删了: 保留 PKColumn 为空, 由下面补列
- case 1:
- s.PKColumn = pkCols[0]
- default:
- return nil, fmt.Errorf("VRTX 表 %s 的行存储索引 %s 有 %d 个分量, 不是合法的行存储索引",
- table, pk.Name(), len(pkCols))
- }
- for _, m := range im.GetIndexes() {
- if m.Name() == s.PKIndexName {
- continue // 主键索引由 PKColumn / PKIndexType 表达
- }
- cols := make([]string, 0, m.FieldCnt)
- broken := false
- for _, fid := range m.Fields() {
- if int(fid) >= len(fields) || util.IsHighBitSet(byte(fields[fid].FieldType)) {
- broken = true // 分量列已被删除: 索引名存实亡, 导过去只会是个坏索引
- break
- }
- cols = append(cols, util.BytesToString(fields[fid].FieldName))
- }
- if broken || len(cols) == 0 {
- v.opts.logf("[vrtx] 表 %s 的索引 %s 有分量列已被删除, 跳过", table, m.Name())
- continue
- }
- s.Indexes = append(s.Indexes, IndexSchema{
- Name: m.Name(),
- Type: m.IndexType,
- Columns: cols,
- Unique: m.Unique(), // IndexMeta.Flag 的唯一位: 约束的执行者
- })
- }
- // 时序索引: 行存储的键是"时间列", 与行号无关 —— 既不能当 SQL 主键 (主键得是整数),
- // 也不能把行号往时间列里写。这类表导出到 SQL 时不设主键, 时间列照常是普通列;
- // 行存储索引的类型和键列记在 __vrtx_meta 里, 回来时原样重建。
- //
- // 顺带说明为什么不给时间列补 _id 或普通索引: 时序索引自己维护的 pkID 并不可靠
- // (首个分段落盘时会把首列数据当成 pkID 存, 见 datapage.CreateDataPage), 所以这条
- // 路径上干脆不读它, 让目标端按自增号分配; 时间列是行存储键, 建普通索引也是重复。
- if s.PKIndexType == page.IndexTypeTime && s.PKColumn != "" &&
- pinRowStorePK(s, s.PKColumn) {
- s.RowStoreColumns = append([]string(nil), pkCols...)
- s.PKColumn = ""
- return s, nil
- }
- // 其余情况行主键必须能承载行号 (SQL 的 PRIMARY KEY 只能是整数, VRTX 侧这个列
- // 也是 pkID 的镜像)。行存储索引挂在定长串这类非 8 字节列上时, 该列的值与行号
- // 不是一回事 —— 补一个 _id 列专门承载行号, 原列退化成普通列。
- //
- // pinRowStorePK 顺带把"有符号 8 字节整数"钉回 uint64 档 (见 rowStorePKType)。
- if !pinRowStorePK(s, s.PKColumn) {
- name := freeColumnName(s, "_id")
- s.Columns = append(s.Columns, Column{Name: name, Type: types.FieldType_Uint64})
- s.MarkSynthetic(name)
- if s.PKColumn != "" {
- v.opts.logf("[vrtx] 表 %s 的行存储索引挂在 %s (%s) 上, 不是 8 字节整数列, "+
- "补列 %s 承载行号", table, s.PKColumn, TypeName(typeOfColumn(s, s.PKColumn)), name)
- // 原列变成普通列, 不参与主键; 但"按它查"是原来的用法, 补个索引续上
- ensureColumnIndex(s, s.PKColumn)
- }
- s.PKIndexType = page.IndexTypePrimary
- s.PKColumn = name
- }
- return s, nil
- }
- // Scan 全表扫描。回调拿到的 Row.Values 与 s.Columns 同序。
- func (v *VRTX) Scan(table string, s *TableSchema, fn func(Row) error) error {
- tbl, ok := v.db.FindTable(table)
- if !ok {
- return fmt.Errorf("VRTX 库 %s 里没有表 %s", v.path, table)
- }
- pos := make(map[string]int, len(s.Columns))
- for i, c := range s.Columns {
- pos[c.Name] = i
- }
- pkIdx := columnIndex(s, s.PKColumn)
- diverged := false
- var fnErr error
- scanErr := tbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
- row := Row{PK: pkID, HasPK: true, Values: make([]any, len(s.Columns))}
- for _, fv := range vals {
- i, ok := pos[util.BytesToString(fv.FieldName)]
- if !ok {
- continue // 已删除的列 / 不在导出范围内的列
- }
- // Blob 列的行内值只有 8 字节 BlobRef (指向链页), 而中间表示要的是【内容本身】:
- // 在这一层解引用一次, 下游的 coerceForSQL / coerceForVRTX 就只面对 []byte,
- // 不必各自认识链引用。
- if ref, ok := fv.Value.(types.BlobRef); ok {
- data, err := tbl.BlobRead(ref, types.CompressNone)
- if err != nil {
- fnErr = fmt.Errorf("表 %s 读回 blob 列 %s (ref=%+v): %w",
- table, util.BytesToString(fv.FieldName), ref, err)
- return false
- }
- if data == nil {
- data = []byte{}
- }
- row.Values[i] = data
- continue
- }
- row.Values[i] = fv.Value
- }
- // 主键列以行号为准: 它本来就是 pkID 的镜像。真有对不上的 (绕过 ORM 直接
- // 用 core 写入、主键列给了别的值), 说一声, 免得以为是导错了。
- if pkIdx >= 0 {
- if old := row.Values[pkIdx]; old != nil {
- if n, err := asUint64(old); err == nil && n != pkID && !diverged {
- diverged = true
- v.opts.logf("[vrtx] 表 %s 有行的主键列 %s 与行号不一致 (如 %v ≠ %d), "+
- "导出时以行号为准", table, s.PKColumn, old, pkID)
- }
- }
- row.Values[pkIdx] = pkID
- }
- if err := fn(row); err != nil {
- fnErr = err
- return false
- }
- return true
- })
- if fnErr != nil {
- return fnErr
- }
- if scanErr != nil {
- return fmt.Errorf("扫描 VRTX 表 %s: %w", table, scanErr)
- }
- return nil
- }
- // ============================ Sink ============================
- // Exists 目标表是否存在。
- func (v *VRTX) Exists(table string) (bool, error) {
- _, ok := v.db.FindTable(table)
- return ok, nil
- }
- // CreateTable 建表 + 建索引 (主键索引必须建, 否则行数据无处安放)。
- func (v *VRTX) CreateTable(s *TableSchema, recreate bool) error {
- exists, err := v.Exists(s.Name)
- if err != nil {
- return err
- }
- if exists && recreate {
- if err := v.db.DropTable(s.Name); err != nil {
- return fmt.Errorf("DROP VRTX 表 %s: %w", s.Name, err)
- }
- exists = false
- }
- if exists {
- return nil // 复用已有表: 列与索引保持原样, 数据追加
- }
- if len(s.RowStore()) == 0 {
- return fmt.Errorf("VRTX 表 %s 没有行存储索引的列 (主键列为空), 无法建表", s.Name)
- }
- if len(s.Name) > core.TableNameSize {
- return fmt.Errorf("表名 %s 有 %d 字节, 超过 VRTX 上限 %d", s.Name, len(s.Name), core.TableNameSize)
- }
- // 单列唯一索引 ⇒ 这一列同时是"唯一列", 把列上的声明位补上。
- //
- // 声明与约束是两件事 (列位给读者看, 索引负责拦), 但只有索引这一半能从 SQL 读
- // 出来 —— SQLite 的列级 UNIQUE 在 PRAGMA table_info 里毫无痕迹, 只生成一条
- // autoindex。补位不会凭空多出约束 (约束一直是索引给的), 少了它却会让从外部库
- // 迁进来的表"看不出这列本该唯一"。复合唯一索引不补: (A,B) 唯一不等于 B 唯一。
- for _, idx := range s.Indexes {
- if !idx.Unique || len(idx.Columns) != 1 {
- continue
- }
- if ci := columnIndex(s, idx.Columns[0]); ci >= 0 {
- s.Columns[ci].Unique = true
- }
- }
- tbl, err := v.db.CreateTable(s.Name, vrtxFields(s))
- if err != nil {
- return fmt.Errorf("建 VRTX 表 %s: %w", s.Name, err)
- }
- rsCols := s.RowStore()
- if len(rsCols) == 0 {
- return fmt.Errorf("表 %s 没有行存储索引的列 (主键列为空)", s.Name)
- }
- pkType := s.PKIndexType
- if pkType != page.IndexTypeTime {
- pkType = page.IndexTypePrimary
- }
- ids := make([]byte, 0, len(rsCols))
- for _, col := range rsCols {
- ci := columnIndex(s, col)
- if ci < 0 {
- return fmt.Errorf("表 %s 的行存储索引列 %s 不在列清单里", s.Name, col)
- }
- if pkType == page.IndexTypeTime && s.Columns[ci].Type != types.FieldType_Uint64 {
- // 时序索引的键就是 8 字节时间戳, 宽度不对会在写入时直接崩。
- return fmt.Errorf("表 %s 的行存储索引列 %s 是 %s, 时序索引要求 8 字节整数列",
- s.Name, col, TypeName(s.Columns[ci].Type))
- }
- ids = append(ids, byte(ci))
- }
- pkName := s.PKIndexName
- if pkName == "" {
- pkName = "pk_" + s.Name
- }
- if err := checkIndexName(pkName); err != nil {
- return err
- }
- if _, err := tbl.CreateIndex(pkName, pkType, ids); err != nil {
- return fmt.Errorf("建主键索引 %s: %w", pkName, err)
- }
- for _, idx := range s.Indexes {
- if idx.Name == pkName || idx.IsRowStore() {
- continue
- }
- // 索引名超长 / 分量过多: 与下面的"动态列当键"同一处理 —— 跳过并记进报告。
- // 这两种都是"源库合法、VRTX 装不下" (SQLite 的索引名与列数几乎没有限制), 但都
- // 只影响这条索引, 不该让整张表的数据陪葬。以前这里直接 return err, 于是一个
- // 名字太长的索引能把整张表拦下来。
- if bad := auditIndexForVRTX(idx); bad != "" {
- s.SkippedIndexes = append(s.SkippedIndexes, idx.Name+": "+bad)
- v.opts.logf("[vrtx] 表 %s 的索引 %s 建不了, 跳过 (%s)", s.Name, idx.Name, bad)
- continue
- }
- ids := make([]byte, 0, len(idx.Columns))
- bad := ""
- for _, col := range idx.Columns {
- ci := columnIndex(s, col)
- if ci < 0 {
- bad = "列 " + col + " 不在表中"
- break
- }
- if s.Columns[ci].IsDynamic() {
- // 源库里它是条合法索引 (MySQL 的长 varchar / text 都能建), 但 VRTX
- // 行内只存动态列的堆引用, 按值查不到, 引擎会直接拒 (ErrIndexDynamicField)。
- // 跳过这个索引, 而不是让整张表失败 —— 索引丢了还能补, 数据没迁才是真丢。
- bad = fmt.Sprintf("键列 %s 是 %s 动态列, VRTX 不能拿它当索引键",
- col, TypeName(s.Columns[ci].Type))
- break
- }
- if s.Columns[ci].Type == types.FieldType_Blob {
- // 同理 (v10 起 Blob 的行内是 8 字节 BlobRef): 索引里会变成页号而不是内容,
- // 引擎在 CreateIndex 直接拒 (ErrIndexBlobField)。这里提前跳过, 免得一个
- // BLOB 列上的索引把整张表的迁移拦下来。
- bad = fmt.Sprintf("键列 %s 是 blob 列, VRTX 不能拿它当索引键 (行内只有 8 字节链引用)",
- col)
- break
- }
- ids = append(ids, byte(ci))
- }
- if bad != "" {
- s.SkippedIndexes = append(s.SkippedIndexes, idx.Name+": "+bad)
- v.opts.logf("[vrtx] 表 %s 的索引 %s 建不了, 跳过 (%s)", s.Name, idx.Name, bad)
- continue
- }
- idxType := idx.Type
- if idxType == 0 || idxType == page.IndexTypePrimary || idxType == page.IndexTypeTime {
- idxType = page.IndexTypeBTree
- }
- if _, err := tbl.CreateIndex(idx.Name, idxType, ids, idx.Unique); err != nil {
- return fmt.Errorf("建索引 %s: %w", idx.Name, err)
- }
- }
- return nil
- }
- // Open 准备写入。
- func (v *VRTX) Open(table string, s *TableSchema) (RowWriter, error) {
- tbl, ok := v.db.FindTable(table)
- if !ok {
- return nil, fmt.Errorf("VRTX 库 %s 里没有表 %s (要先建表)", v.path, table)
- }
- return &vrtxWriter{tbl: tbl, schema: s, opts: v.opts, pkIdx: columnIndex(s, s.PKColumn)}, nil
- }
- type vrtxWriter struct {
- tbl *core.Table
- schema *TableSchema
- opts *Options
- pkIdx int
- }
- // Write 逐行写入。VRTX 没有"批量插入"接口 (每行都要维护 B+ 树与索引项),
- // 所以这里就是 for 循环 —— 批的意义在于 SQL 侧的往返次数。
- func (w *vrtxWriter) Write(rows []Row) error {
- vals := make([]types.FieldValue, len(w.schema.Columns))
- for i, c := range w.schema.Columns {
- vals[i].FieldName = []byte(c.Name)
- }
- for ri := range rows {
- r := &rows[ri]
- pkID := r.PK
- if !r.HasPK || pkID == 0 {
- pkID = w.tbl.AllocPKID()
- }
- for i, c := range w.schema.Columns {
- var raw any
- if i < len(r.Values) {
- raw = r.Values[i]
- }
- if i == w.pkIdx {
- raw = pkID // 主键列是行号的镜像
- }
- cv, err := coerceForVRTX(c.Type, c.Param, raw, c.Name, w.opts.StrictValue, c.Nullable)
- if err != nil {
- return fmt.Errorf("%s: %w", w.rowTag(r, ri), err)
- }
- if w.opts.TruncateOversize {
- cv = w.truncateOversize(cv, c, r, ri)
- }
- vals[i].Value = cv
- }
- if _, err := w.tbl.InsertRowWithID(pkID, vals); err != nil {
- return fmt.Errorf("%s: 写入 VRTX 表 %s: %w", w.rowTag(r, ri), w.schema.Name, err)
- }
- }
- return nil
- }
- // rowTag 给写失败的那一行拼一个"回源库找得到它"的标识。
- //
- // 迁移报错时用户真正要做的动作是回源库处理那一行 (把列改大 / 把值裁小 / 剔掉), 只报
- // "表 x 的 y 列超限"等于让人拿 LENGTH(y) 去全表捞 —— 动态列的容量上限与源库的声明无关,
- // 光看列名反推不出是哪一行。
- //
- // 三种身份按可靠程度排:
- // 1. 源库主键列的值: 源库自己认得, 能直接 SELECT ... WHERE id = ?。主键不是 8 字节整数、
- // 行号被换成引擎分配的 _id 的表 (MySQL 常见的 int(11) 主键都走这条) 只能靠它;
- // 2. VRTX 行号: 源库主键本身就是行号时, 它等于源库主键值, 一样能定位;
- // 3. 批内序号: 前两条都拿不到 (源表没有整型主键) 时的兜底, 只能把范围缩到那一批。
- func (w *vrtxWriter) rowTag(r *Row, batchPos int) string {
- if i := columnIndex(w.schema, w.schema.SourcePKColumn); i >= 0 && i < len(r.Values) {
- if v := r.Values[i]; v != nil {
- if b, ok := v.([]byte); ok {
- return fmt.Sprintf("源表主键 %s", b)
- }
- return fmt.Sprintf("源表主键 %v", v)
- }
- }
- if r.HasPK {
- return fmt.Sprintf("主键 %d", r.PK)
- }
- return fmt.Sprintf("批内第 %d 行", batchPos+1)
- }
- // truncateOversize 把超过单值上限的 Dynamicbyte 值截到上限, 并逐行报出截了什么 (见
- // Options.TruncateOversize 说明为什么这不是默认行为)。
- //
- // 只动 Dynamicbyte, 也只动"超过单值上限"这一种: 定长列装不下是另一码事, 归 StrictValue /
- // -loose 管; blob 列的内容在链上 (单值约 4GB), 没有这个上限。
- func (w *vrtxWriter) truncateOversize(v any, c Column, r *Row, batchPos int) any {
- if !types.IsDynamic(c.Type) {
- return v
- }
- b, ok := v.([]byte)
- if !ok || uint32(len(b)) <= core.MaxBlobSize {
- return v
- }
- // 用的是引擎那道闸门的同一个常量 (core.MaxBlobSize), 不是迁移侧自己的推算 ——
- // 截到"闸门允许的最大值"才既装得下又不白丢。
- cut := runeSafeCut(b, int(core.MaxBlobSize))
- w.opts.logf("[migrate] 表 %s %s 列 %s 的值 %d 字节超过单值上限 %d, 已截成 %d 字节 (丢了 %d 字节)",
- w.schema.Name, w.rowTag(r, batchPos), c.Name, len(b), core.MaxBlobSize, cut, len(b)-cut)
- return b[:cut]
- }
- // runeSafeCut 求一个不超过 limit 的截断位置, 并且不把一个 UTF-8 字符切成两半。
- //
- // 判据是"被排除的第一个字节是不是字符起始字节": 原值本身就是合法 UTF-8 时, 从字符边界
- // 切开得到的必然是合法文本; 二进制列 (本来就不是文本) 退到最近一个非续字节即可, 少
- // 1..3 个字节对它没有意义。
- func runeSafeCut(b []byte, limit int) int {
- if limit >= len(b) {
- return len(b)
- }
- cut := limit
- for cut > 0 && !utf8.RuneStart(b[cut]) {
- cut--
- }
- if cut == 0 {
- // 上限前面全是续字节 —— 这不是文本, 没有字符边界可退, 老实按字节截,
- // 免得把整个值截成空的。
- return limit
- }
- return cut
- }
- // Close 提交这一批 (VRTX 的提交由 DB 层统一做, 这里只留扩展点)。
- func (w *vrtxWriter) Close() error { return nil }
- // ============================ 工具 ============================
- func checkIndexName(name string) error {
- if name == "" {
- return errors.New("索引名为空")
- }
- if len(name) > core.IndexNameSize {
- return fmt.Errorf("索引名 %s 有 %d 字节, 超过 VRTX 上限 %d (换短一点的名字)",
- name, len(name), core.IndexNameSize)
- }
- return nil
- }
- // columnFlag 列的 VRTX 标志位: 低 5 位 scale + NULL / 唯一声明。
- //
- // 三者共用一个字节 (布局见 types 的 Flag* 常量), 少写任何一位都是静默改结构:
- // 丢了 FlagNullable, 往这列写 NULL 会被当成"给个零值"塞 0; 丢了 FlagUnique,
- // 反向推 schema 的工具 (以及 ORM 的 tag) 就看不出这列本该唯一。
- func columnFlag(c Column) byte {
- f := c.Param & types.FlagScaleMask
- if c.Nullable {
- f |= types.FlagNullable
- }
- if c.Unique {
- f |= types.FlagUnique
- }
- return f
- }
- // vrtxFields 把列清单翻成 core 建表要的 []types.FieldInfo。
- //
- // 抽出来是因为它有两个用途, 两处必须逐字节一致 —— 建表用它, 行宽闸门 (更宽的行装不进
- // 一页两行) 也用它算行宽 (见 fitRowWidthForVRTX)。各写一份的话, 一旦某天只改了建表那份
- // (比如新增一个列标志位), 行宽闸门算的就是另一张表: 闸门放过的表建不出来。
- func vrtxFields(s *TableSchema) []types.FieldInfo {
- fields := make([]types.FieldInfo, 0, len(s.Columns))
- for _, c := range s.Columns {
- fields = append(fields, types.FieldInfo{
- FieldType: c.Type,
- FieldName: []byte(c.Name),
- Flag: columnFlag(c), // decimal scale + NULL / 唯一声明
- })
- }
- return fields
- }
- func columnIndex(s *TableSchema, name string) int {
- if i, ok := s.ColumnIdx(name); ok {
- return i
- }
- return -1
- }
- func typeOfColumn(s *TableSchema, name string) types.FieldType {
- if i, ok := s.ColumnIdx(name); ok {
- return s.Columns[i].Type
- }
- return 0
- }
- // rowStorePKType 判断某列能否直接承载行号 (pkID 的镜像), 并给出它在 VRTX 侧必须使用的类型。
- //
- // 行存储索引的键就是一个 8 字节整数列: 它的值即引擎行主键号, 主键 B+ 树按它的字节序排列;
- // 时序表里这一列同时承载时间戳, 时序索引按 int64 补码直读这 8 个字节。因此它在 VRTX 侧
- // 必须落在【无符号 uint64 档】上 —— 盘上就是补码本身。
- //
- // 而 SQL 的整数默认被 inferSQLType 收编到有符号档 (Int64, 盘上是保序编码): 宽度同为 8 字节,
- // 但字节布局不同, 直接拿去当行主键会让主键树错序、当时间键会让时间轴错位。这里把它钉回
- // uint64; 行号本身非负, 值不会因此改变。1/2/4 字节的整数列装不下行号, 返回 false,
- // 由调用方补 _id 列。
- func rowStorePKType(t types.FieldType) (types.FieldType, bool) {
- switch t {
- case types.FieldType_Uint64:
- return types.FieldType_Uint64, true
- case types.FieldType_Int64:
- return types.FieldType_Uint64, true
- }
- return 0, false
- }
- // pinRowStorePK 就地把指定列钉到行存储主键要求的档位; 列不存在或宽度不够时返回 false。
- func pinRowStorePK(s *TableSchema, col string) bool {
- i, ok := s.ColumnIdx(col)
- if !ok {
- return false
- }
- ft, ok := rowStorePKType(s.Columns[i].Type)
- if !ok {
- return false
- }
- s.Columns[i].Type = ft
- return true
- }
- // freeColumnName 找一个没被占用的列名 (优先用 want)。
- func freeColumnName(s *TableSchema, want string) string {
- if !s.HasColumn(want) {
- return want
- }
- for i := 2; ; i++ {
- cand := fmt.Sprintf("%s_%d", want, i)
- if !s.HasColumn(cand) {
- return cand
- }
- }
- }
- // vrtxTableName 把外部表名收拾成 VRTX 能接受的样子 (名字里有空格/奇怪字符时)。
- func vrtxTableName(name string) string {
- return strings.TrimSpace(name)
- }
|