vrtx.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. package migrate
  2. import (
  3. "TsDB2/DB/core"
  4. "TsDB2/DB/core/page"
  5. "TsDB2/DB/core/types"
  6. "TsDB2/DB/core/util"
  7. "errors"
  8. "fmt"
  9. "sort"
  10. "strings"
  11. )
  12. // VRTX 一侧。同一个对象既能当 Source 也能当 Sink (方向由调用方决定), 于是任意
  13. // 方向 (VRTX→SQL / SQL→VRTX / VRTX→VRTX) 都只需要这一套读写实现。
  14. //
  15. // 这里刻意走 core 层而不是 ORM (package DB): 表结构在迁移时来自源库的元数据
  16. // (列名 / 类型 / 行存储索引列), 运行时才知道, 根本没有一个 Go 结构体可以内嵌表基类 ——
  17. // 而 ORM 的入口 UseTable[T] 恰恰要求 T 内嵌 DB.BaseModel / DB.TimeSeriesModel
  18. // (见 DB 包的 requireBase)。core 层只认 []types.FieldInfo, 没有"先有结构体"这个前提,
  19. // 所以"按库里的元数据建表"只能落在 core 层。ORM 是给"写代码时就知道表结构"的场景用的。
  20. type VRTX struct {
  21. db *core.DB
  22. path string
  23. opts *Options
  24. }
  25. // OpenVRTX 打开 (不存在则新建) 一个 VRTX 库文件。
  26. func OpenVRTX(path string, opts *Options) (*VRTX, error) {
  27. db, err := core.Open(path)
  28. if err != nil {
  29. return nil, fmt.Errorf("打开 VRTX 库 %s: %w", path, err)
  30. }
  31. return &VRTX{db: db, path: path, opts: opts.normalized()}, nil
  32. }
  33. func (v *VRTX) Kind() Kind { return KindVRTX }
  34. func (v *VRTX) Identity() string { return "vrtx:" + v.path }
  35. // DB 暴露底层句柄 (调用方可能需要调缓冲池 / 手动提交)。
  36. func (v *VRTX) DB() *core.DB { return v.db }
  37. // Commit 把当前脏页落盘。编排器每迁完一张表调一次: VRTX 的插入是攒在缓冲池里的,
  38. // 千万行的库若要等最后 Close 才提交, 中途崩一次就是全部白干, 内存也会一直涨。
  39. func (v *VRTX) Commit() error { return v.db.Commit() }
  40. // Close 落盘并关闭。
  41. func (v *VRTX) Close() error {
  42. // 迁移是批量写, 收尾必须显式提交一次: 否则最后一批脏页只剩防抖定时器兜底,
  43. // 进程立刻退出就会丢。
  44. if err := v.db.Commit(); err != nil {
  45. _ = v.db.Close()
  46. return err
  47. }
  48. return v.db.Close()
  49. }
  50. // Tables 列出全部表 (core 已跳过软删除的表)。
  51. func (v *VRTX) Tables() ([]string, error) {
  52. metas := v.db.GetAllTables()
  53. names := make([]string, 0, len(metas))
  54. for _, m := range metas {
  55. names = append(names, m.Name)
  56. }
  57. sort.Strings(names) // 稳定顺序: 迁移过程可复现、可对照
  58. return names, nil
  59. }
  60. // Schema 读一张表的结构。
  61. //
  62. // 列取 master registry 里未被软删除的那些 —— 注意用 master 顺序而不是
  63. // GetActiveFieldNames(): 后者是"当前 schema 版本"的列序, 而 master registry 的
  64. // 下标才是索引项里 FieldIDs 引用的那个下标, 换算列名必须用它。
  65. func (v *VRTX) Schema(table string) (*TableSchema, error) {
  66. tbl, ok := v.db.FindTable(table)
  67. if !ok {
  68. return nil, fmt.Errorf("VRTX 库 %s 里没有表 %s", v.path, table)
  69. }
  70. fields := tbl.GetFields() // master registry: 含已软删除的列 (删除标记见 types.FlagDeleted)
  71. s := &TableSchema{Name: table}
  72. for _, f := range fields {
  73. if f.IsDropped() {
  74. continue
  75. }
  76. s.Columns = append(s.Columns, Column{
  77. Name: util.BytesToString(f.FieldName),
  78. Type: f.FieldType,
  79. Param: f.Flag & types.FlagScaleMask, // Decimal 的列级 scale, 必须跟着列走
  80. // NULL / 唯一声明: 前者决定导出时写不写 NOT NULL, 后者是列级声明
  81. // (约束本身由索引执行, 见下面 GetIndexes 的 m.Unique())。
  82. Nullable: f.IsNullable(),
  83. Unique: f.IsUnique(),
  84. })
  85. }
  86. im := tbl.IndexMeta()
  87. pk, hasPK := im.GetRowStoreIndex()
  88. if !hasPK {
  89. return nil, fmt.Errorf("VRTX 表 %s 没有行存储索引 (主键/时序), 行数据无处安放", table)
  90. }
  91. s.PKIndexName = pk.Name()
  92. s.PKIndexType = pk.IndexType
  93. pkCols := make([]string, 0, pk.FieldCnt)
  94. for _, fid := range pk.Fields() {
  95. if int(fid) >= len(fields) {
  96. continue
  97. }
  98. if util.IsHighBitSet(byte(fields[fid].FieldType)) {
  99. continue
  100. }
  101. pkCols = append(pkCols, util.BytesToString(fields[fid].FieldName))
  102. }
  103. switch len(pkCols) {
  104. case 0:
  105. // 行存储索引的分量列全被删了: 保留 PKColumn 为空, 由下面补列
  106. case 1:
  107. s.PKColumn = pkCols[0]
  108. default:
  109. return nil, fmt.Errorf("VRTX 表 %s 的行存储索引 %s 有 %d 个分量, 不是合法的行存储索引",
  110. table, pk.Name(), len(pkCols))
  111. }
  112. for _, m := range im.GetIndexes() {
  113. if m.Name() == s.PKIndexName {
  114. continue // 主键索引由 PKColumn / PKIndexType 表达
  115. }
  116. cols := make([]string, 0, m.FieldCnt)
  117. broken := false
  118. for _, fid := range m.Fields() {
  119. if int(fid) >= len(fields) || util.IsHighBitSet(byte(fields[fid].FieldType)) {
  120. broken = true // 分量列已被删除: 索引名存实亡, 导过去只会是个坏索引
  121. break
  122. }
  123. cols = append(cols, util.BytesToString(fields[fid].FieldName))
  124. }
  125. if broken || len(cols) == 0 {
  126. v.opts.logf("[vrtx] 表 %s 的索引 %s 有分量列已被删除, 跳过", table, m.Name())
  127. continue
  128. }
  129. s.Indexes = append(s.Indexes, IndexSchema{
  130. Name: m.Name(),
  131. Type: m.IndexType,
  132. Columns: cols,
  133. Unique: m.Unique(), // IndexMeta.Flag 的唯一位: 约束的执行者
  134. })
  135. }
  136. // 时序索引: 行存储的键是"时间列", 与行号无关 —— 既不能当 SQL 主键 (主键得是整数),
  137. // 也不能把行号往时间列里写。这类表导出到 SQL 时不设主键, 时间列照常是普通列;
  138. // 行存储索引的类型和键列记在 __vrtx_meta 里, 回来时原样重建。
  139. //
  140. // 顺带说明为什么不给时间列补 _id 或普通索引: 时序索引自己维护的 pkID 并不可靠
  141. // (首个分段落盘时会把首列数据当成 pkID 存, 见 datapage.CreateDataPage), 所以这条
  142. // 路径上干脆不读它, 让目标端按自增号分配; 时间列是行存储键, 建普通索引也是重复。
  143. if s.PKIndexType == page.IndexTypeTime && s.PKColumn != "" &&
  144. pinRowStorePK(s, s.PKColumn) {
  145. s.RowStoreColumns = append([]string(nil), pkCols...)
  146. s.PKColumn = ""
  147. return s, nil
  148. }
  149. // 其余情况行主键必须能承载行号 (SQL 的 PRIMARY KEY 只能是整数, VRTX 侧这个列
  150. // 也是 pkID 的镜像)。行存储索引挂在定长串这类非 8 字节列上时, 该列的值与行号
  151. // 不是一回事 —— 补一个 _id 列专门承载行号, 原列退化成普通列。
  152. //
  153. // pinRowStorePK 顺带把"有符号 8 字节整数"钉回 8byte 档 (见 rowStorePKType)。
  154. if !pinRowStorePK(s, s.PKColumn) {
  155. name := freeColumnName(s, "_id")
  156. s.Columns = append(s.Columns, Column{Name: name, Type: types.FieldType_8byte})
  157. s.MarkSynthetic(name)
  158. if s.PKColumn != "" {
  159. v.opts.logf("[vrtx] 表 %s 的行存储索引挂在 %s (%s) 上, 不是 8 字节整数列, "+
  160. "补列 %s 承载行号", table, s.PKColumn, TypeName(typeOfColumn(s, s.PKColumn)), name)
  161. // 原列变成普通列, 不参与主键; 但"按它查"是原来的用法, 补个索引续上
  162. ensureColumnIndex(s, s.PKColumn)
  163. }
  164. s.PKIndexType = page.IndexTypePrimary
  165. s.PKColumn = name
  166. }
  167. return s, nil
  168. }
  169. // Scan 全表扫描。回调拿到的 Row.Values 与 s.Columns 同序。
  170. func (v *VRTX) Scan(table string, s *TableSchema, fn func(Row) error) error {
  171. tbl, ok := v.db.FindTable(table)
  172. if !ok {
  173. return fmt.Errorf("VRTX 库 %s 里没有表 %s", v.path, table)
  174. }
  175. pos := make(map[string]int, len(s.Columns))
  176. for i, c := range s.Columns {
  177. pos[c.Name] = i
  178. }
  179. pkIdx := columnIndex(s, s.PKColumn)
  180. diverged := false
  181. var fnErr error
  182. scanErr := tbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
  183. row := Row{PK: pkID, HasPK: true, Values: make([]any, len(s.Columns))}
  184. for _, fv := range vals {
  185. i, ok := pos[util.BytesToString(fv.FieldName)]
  186. if !ok {
  187. continue // 已删除的列 / 不在导出范围内的列
  188. }
  189. row.Values[i] = fv.Value
  190. }
  191. // 主键列以行号为准: 它本来就是 pkID 的镜像。真有对不上的 (绕过 ORM 直接
  192. // 用 core 写入、主键列给了别的值), 说一声, 免得以为是导错了。
  193. if pkIdx >= 0 {
  194. if old := row.Values[pkIdx]; old != nil {
  195. if n, err := asUint64(old); err == nil && n != pkID && !diverged {
  196. diverged = true
  197. v.opts.logf("[vrtx] 表 %s 有行的主键列 %s 与行号不一致 (如 %v ≠ %d), "+
  198. "导出时以行号为准", table, s.PKColumn, old, pkID)
  199. }
  200. }
  201. row.Values[pkIdx] = pkID
  202. }
  203. if err := fn(row); err != nil {
  204. fnErr = err
  205. return false
  206. }
  207. return true
  208. })
  209. if fnErr != nil {
  210. return fnErr
  211. }
  212. if scanErr != nil {
  213. return fmt.Errorf("扫描 VRTX 表 %s: %w", table, scanErr)
  214. }
  215. return nil
  216. }
  217. // ============================ Sink ============================
  218. // Exists 目标表是否存在。
  219. func (v *VRTX) Exists(table string) (bool, error) {
  220. _, ok := v.db.FindTable(table)
  221. return ok, nil
  222. }
  223. // CreateTable 建表 + 建索引 (主键索引必须建, 否则行数据无处安放)。
  224. func (v *VRTX) CreateTable(s *TableSchema, recreate bool) error {
  225. exists, err := v.Exists(s.Name)
  226. if err != nil {
  227. return err
  228. }
  229. if exists && recreate {
  230. if err := v.db.DropTable(s.Name); err != nil {
  231. return fmt.Errorf("DROP VRTX 表 %s: %w", s.Name, err)
  232. }
  233. exists = false
  234. }
  235. if exists {
  236. return nil // 复用已有表: 列与索引保持原样, 数据追加
  237. }
  238. if len(s.RowStore()) == 0 {
  239. return fmt.Errorf("VRTX 表 %s 没有行存储索引的列 (主键列为空), 无法建表", s.Name)
  240. }
  241. if len(s.Name) > core.TableNameSize {
  242. return fmt.Errorf("表名 %s 有 %d 字节, 超过 VRTX 上限 %d", s.Name, len(s.Name), core.TableNameSize)
  243. }
  244. // 单列唯一索引 ⇒ 这一列同时是"唯一列", 把列上的声明位补上。
  245. //
  246. // 声明与约束是两件事 (列位给读者看, 索引负责拦), 但只有索引这一半能从 SQL 读
  247. // 出来 —— SQLite 的列级 UNIQUE 在 PRAGMA table_info 里毫无痕迹, 只生成一条
  248. // autoindex。补位不会凭空多出约束 (约束一直是索引给的), 少了它却会让从外部库
  249. // 迁进来的表"看不出这列本该唯一"。复合唯一索引不补: (A,B) 唯一不等于 B 唯一。
  250. for _, idx := range s.Indexes {
  251. if !idx.Unique || len(idx.Columns) != 1 {
  252. continue
  253. }
  254. if ci := columnIndex(s, idx.Columns[0]); ci >= 0 {
  255. s.Columns[ci].Unique = true
  256. }
  257. }
  258. tbl, err := v.db.CreateTable(s.Name, vrtxFields(s))
  259. if err != nil {
  260. return fmt.Errorf("建 VRTX 表 %s: %w", s.Name, err)
  261. }
  262. rsCols := s.RowStore()
  263. if len(rsCols) == 0 {
  264. return fmt.Errorf("表 %s 没有行存储索引的列 (主键列为空)", s.Name)
  265. }
  266. pkType := s.PKIndexType
  267. if pkType != page.IndexTypeTime {
  268. pkType = page.IndexTypePrimary
  269. }
  270. ids := make([]byte, 0, len(rsCols))
  271. for _, col := range rsCols {
  272. ci := columnIndex(s, col)
  273. if ci < 0 {
  274. return fmt.Errorf("表 %s 的行存储索引列 %s 不在列清单里", s.Name, col)
  275. }
  276. if pkType == page.IndexTypeTime && s.Columns[ci].Type != types.FieldType_8byte {
  277. // 时序索引的键就是 8 字节时间戳, 宽度不对会在写入时直接崩。
  278. return fmt.Errorf("表 %s 的行存储索引列 %s 是 %s, 时序索引要求 8 字节整数列",
  279. s.Name, col, TypeName(s.Columns[ci].Type))
  280. }
  281. ids = append(ids, byte(ci))
  282. }
  283. pkName := s.PKIndexName
  284. if pkName == "" {
  285. pkName = "pk_" + s.Name
  286. }
  287. if err := checkIndexName(pkName); err != nil {
  288. return err
  289. }
  290. if _, err := tbl.CreateIndex(pkName, pkType, ids); err != nil {
  291. return fmt.Errorf("建主键索引 %s: %w", pkName, err)
  292. }
  293. for _, idx := range s.Indexes {
  294. if idx.Name == pkName || idx.IsRowStore() {
  295. continue
  296. }
  297. // 索引名超长 / 分量过多: 与下面的"动态列当键"同一处理 —— 跳过并记进报告。
  298. // 这两种都是"源库合法、VRTX 装不下" (SQLite 的索引名与列数几乎没有限制), 但都
  299. // 只影响这条索引, 不该让整张表的数据陪葬。以前这里直接 return err, 于是一个
  300. // 名字太长的索引能把整张表拦下来。
  301. if bad := auditIndexForVRTX(idx); bad != "" {
  302. s.SkippedIndexes = append(s.SkippedIndexes, idx.Name+": "+bad)
  303. v.opts.logf("[vrtx] 表 %s 的索引 %s 建不了, 跳过 (%s)", s.Name, idx.Name, bad)
  304. continue
  305. }
  306. ids := make([]byte, 0, len(idx.Columns))
  307. bad := ""
  308. for _, col := range idx.Columns {
  309. ci := columnIndex(s, col)
  310. if ci < 0 {
  311. bad = "列 " + col + " 不在表中"
  312. break
  313. }
  314. if s.Columns[ci].IsDynamic() {
  315. // 源库里它是条合法索引 (MySQL 的长 varchar / text 都能建), 但 VRTX
  316. // 行内只存动态列的堆引用, 按值查不到, 引擎会直接拒 (ErrIndexDynamicField)。
  317. // 跳过这个索引, 而不是让整张表失败 —— 索引丢了还能补, 数据没迁才是真丢。
  318. bad = fmt.Sprintf("键列 %s 是 %s 动态列, VRTX 不能拿它当索引键",
  319. col, TypeName(s.Columns[ci].Type))
  320. break
  321. }
  322. ids = append(ids, byte(ci))
  323. }
  324. if bad != "" {
  325. s.SkippedIndexes = append(s.SkippedIndexes, idx.Name+": "+bad)
  326. v.opts.logf("[vrtx] 表 %s 的索引 %s 建不了, 跳过 (%s)", s.Name, idx.Name, bad)
  327. continue
  328. }
  329. idxType := idx.Type
  330. if idxType == 0 || idxType == page.IndexTypePrimary || idxType == page.IndexTypeTime {
  331. idxType = page.IndexTypeBTree
  332. }
  333. if _, err := tbl.CreateIndex(idx.Name, idxType, ids, idx.Unique); err != nil {
  334. return fmt.Errorf("建索引 %s: %w", idx.Name, err)
  335. }
  336. }
  337. return nil
  338. }
  339. // Open 准备写入。
  340. func (v *VRTX) Open(table string, s *TableSchema) (RowWriter, error) {
  341. tbl, ok := v.db.FindTable(table)
  342. if !ok {
  343. return nil, fmt.Errorf("VRTX 库 %s 里没有表 %s (要先建表)", v.path, table)
  344. }
  345. return &vrtxWriter{tbl: tbl, schema: s, opts: v.opts, pkIdx: columnIndex(s, s.PKColumn)}, nil
  346. }
  347. type vrtxWriter struct {
  348. tbl *core.Table
  349. schema *TableSchema
  350. opts *Options
  351. pkIdx int
  352. }
  353. // Write 逐行写入。VRTX 没有"批量插入"接口 (每行都要维护 B+ 树与索引项),
  354. // 所以这里就是 for 循环 —— 批的意义在于 SQL 侧的往返次数。
  355. func (w *vrtxWriter) Write(rows []Row) error {
  356. vals := make([]types.FieldValue, len(w.schema.Columns))
  357. for i, c := range w.schema.Columns {
  358. vals[i].FieldName = []byte(c.Name)
  359. }
  360. for ri := range rows {
  361. r := &rows[ri]
  362. pkID := r.PK
  363. if !r.HasPK || pkID == 0 {
  364. pkID = w.tbl.AllocPKID()
  365. }
  366. for i, c := range w.schema.Columns {
  367. var raw any
  368. if i < len(r.Values) {
  369. raw = r.Values[i]
  370. }
  371. if i == w.pkIdx {
  372. raw = pkID // 主键列是行号的镜像
  373. }
  374. cv, err := coerceForVRTX(c.Type, c.Param, raw, c.Name, w.opts.StrictValue, c.Nullable)
  375. if err != nil {
  376. return err
  377. }
  378. vals[i].Value = cv
  379. }
  380. if _, err := w.tbl.InsertRowWithID(pkID, vals); err != nil {
  381. return fmt.Errorf("写入 VRTX 表 %s: %w", w.schema.Name, err)
  382. }
  383. }
  384. return nil
  385. }
  386. // Close 提交这一批 (VRTX 的提交由 DB 层统一做, 这里只留扩展点)。
  387. func (w *vrtxWriter) Close() error { return nil }
  388. // ============================ 工具 ============================
  389. func checkIndexName(name string) error {
  390. if name == "" {
  391. return errors.New("索引名为空")
  392. }
  393. if len(name) > core.IndexNameSize {
  394. return fmt.Errorf("索引名 %s 有 %d 字节, 超过 VRTX 上限 %d (换短一点的名字)",
  395. name, len(name), core.IndexNameSize)
  396. }
  397. return nil
  398. }
  399. // columnFlag 列的 VRTX 标志位: 低 5 位 scale + NULL / 唯一声明。
  400. //
  401. // 三者共用一个字节 (布局见 types 的 Flag* 常量), 少写任何一位都是静默改结构:
  402. // 丢了 FlagNullable, 往这列写 NULL 会被当成"给个零值"塞 0; 丢了 FlagUnique,
  403. // 反向推 schema 的工具 (以及 ORM 的 tag) 就看不出这列本该唯一。
  404. func columnFlag(c Column) byte {
  405. f := c.Param & types.FlagScaleMask
  406. if c.Nullable {
  407. f |= types.FlagNullable
  408. }
  409. if c.Unique {
  410. f |= types.FlagUnique
  411. }
  412. return f
  413. }
  414. // vrtxFields 把列清单翻成 core 建表要的 []types.FieldInfo。
  415. //
  416. // 抽出来是因为它有两个用途, 两处必须逐字节一致 —— 建表用它, 行宽闸门 (更宽的行装不进
  417. // 一页两行) 也用它算行宽 (见 fitRowWidthForVRTX)。各写一份的话, 一旦某天只改了建表那份
  418. // (比如新增一个列标志位), 行宽闸门算的就是另一张表: 闸门放过的表建不出来。
  419. func vrtxFields(s *TableSchema) []types.FieldInfo {
  420. fields := make([]types.FieldInfo, 0, len(s.Columns))
  421. for _, c := range s.Columns {
  422. fields = append(fields, types.FieldInfo{
  423. FieldType: c.Type,
  424. FieldName: []byte(c.Name),
  425. Flag: columnFlag(c), // decimal scale + NULL / 唯一声明
  426. })
  427. }
  428. return fields
  429. }
  430. func columnIndex(s *TableSchema, name string) int {
  431. if i, ok := s.ColumnIdx(name); ok {
  432. return i
  433. }
  434. return -1
  435. }
  436. func typeOfColumn(s *TableSchema, name string) types.FieldType {
  437. if i, ok := s.ColumnIdx(name); ok {
  438. return s.Columns[i].Type
  439. }
  440. return 0
  441. }
  442. // rowStorePKType 判断某列能否直接承载行号 (pkID 的镜像), 并给出它在 VRTX 侧必须使用的类型。
  443. //
  444. // 行存储索引的键就是一个 8 字节整数列: 它的值即引擎行主键号, 主键 B+ 树按它的字节序排列;
  445. // 时序表里这一列同时承载时间戳, 时序索引按 int64 补码直读这 8 个字节。因此它在 VRTX 侧
  446. // 必须落在【无符号 8byte 档】上 —— 盘上就是补码本身。
  447. //
  448. // 而 SQL 的整数默认被 inferSQLType 收编到有符号档 (Int64, 盘上是保序编码): 宽度同为 8 字节,
  449. // 但字节布局不同, 直接拿去当行主键会让主键树错序、当时间键会让时间轴错位。这里把它钉回
  450. // 8byte; 行号本身非负, 值不会因此改变。1/2/4 字节的整数列装不下行号, 返回 false,
  451. // 由调用方补 _id 列。
  452. func rowStorePKType(t types.FieldType) (types.FieldType, bool) {
  453. switch t {
  454. case types.FieldType_8byte:
  455. return types.FieldType_8byte, true
  456. case types.FieldType_Int64:
  457. return types.FieldType_8byte, true
  458. }
  459. return 0, false
  460. }
  461. // pinRowStorePK 就地把指定列钉到行存储主键要求的档位; 列不存在或宽度不够时返回 false。
  462. func pinRowStorePK(s *TableSchema, col string) bool {
  463. i, ok := s.ColumnIdx(col)
  464. if !ok {
  465. return false
  466. }
  467. ft, ok := rowStorePKType(s.Columns[i].Type)
  468. if !ok {
  469. return false
  470. }
  471. s.Columns[i].Type = ft
  472. return true
  473. }
  474. // freeColumnName 找一个没被占用的列名 (优先用 want)。
  475. func freeColumnName(s *TableSchema, want string) string {
  476. if !s.HasColumn(want) {
  477. return want
  478. }
  479. for i := 2; ; i++ {
  480. cand := fmt.Sprintf("%s_%d", want, i)
  481. if !s.HasColumn(cand) {
  482. return cand
  483. }
  484. }
  485. }
  486. // vrtxTableName 把外部表名收拾成 VRTX 能接受的样子 (名字里有空格/奇怪字符时)。
  487. func vrtxTableName(name string) string {
  488. return strings.TrimSpace(name)
  489. }