| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- package migrate
- import (
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- "fmt"
- "strconv"
- "strings"
- "testing"
- )
- // ============================ SQL 类型 ↔ VRTX 类型 ============================
- //
- // 这一层是迁移的"翻译表", 出错的后果不是报错而是【静默改变列的语义】:
- // - BLOB 认成 TEXT → 二进制内容进了文本列;
- // - FLOAT 在 SQLite/MySQL 上按同一档收 → 单精度列被当成双精度 (或反过来);
- // - DECIMAL(18,2) 只读类型名不读 (p,s) → 小数位被抹平, 金额变成整数。
- //
- // 所以这里既钉住逐条映射, 也钉住"声明出去再读回来还是同一个类型"这个往返性质。
- func TestInferSQLType(t *testing.T) {
- cases := []struct {
- decl string
- mysql bool
- ft types.FieldType
- param byte
- }{
- // 整数: 有符号 / 无符号分档, SQLite 的 INTEGER 一律按 8 字节收
- {"TINYINT", true, types.FieldType_Int8, 0},
- {"SMALLINT", true, types.FieldType_Int16, 0},
- {"INT", true, types.FieldType_Int32, 0},
- {"MEDIUMINT", true, types.FieldType_Int32, 0},
- {"BIGINT", true, types.FieldType_Int64, 0},
- {"INTEGER", false, types.FieldType_Int64, 0},
- {"TINYINT UNSIGNED", true, types.FieldType_Uint8, 0},
- {"SMALLINT UNSIGNED", true, types.FieldType_Uint16, 0},
- {"INT UNSIGNED", true, types.FieldType_Uint32, 0},
- {"BIGINT UNSIGNED", true, types.FieldType_Uint64, 0},
- // 浮点: MySQL 的 FLOAT 就是 4 字节, SQLite 只有 REAL 亲和性 (8 字节)
- {"FLOAT", true, types.FieldType_Float32, 0},
- {"FLOAT", false, types.FieldType_Float64, 0},
- {"DOUBLE", true, types.FieldType_Float64, 0},
- {"REAL", false, types.FieldType_Float64, 0},
- // 定点: (p,s) 必须读进来 —— 只读类型名会把小数位抹平
- {"DECIMAL(18,2)", true, types.FieldType_Decimal, 2},
- {"DECIMAL(10,0)", true, types.FieldType_Decimal, 0},
- {"NUMERIC(20,6)", false, types.FieldType_Decimal, 6},
- // p 与 s 是【两个不同的上界】: p ≤ 38 (128 位系数的十进制容量),
- // s ≤ 31 (Flag 低 5 位)。以前拿 31 去卡 p, 于是 DECIMAL(38,2) 被误判成
- // "表达不了"而退成 float64 —— 那是被写错的上限限制的, 不是被能力限制的。
- {"DECIMAL(38,2)", true, types.FieldType_Decimal, 2},
- {"DECIMAL(38,31)", true, types.FieldType_Decimal, 31},
- // 真的超出系数容量 → 退回 Float64 (与改动前一致), 不假装精确; 但要报出来 (degradeNote)
- {"DECIMAL(65,30)", true, types.FieldType_Float64, 0},
- {"DECIMAL(39,0)", true, types.FieldType_Float64, 0},
- // s 超 31: 系数装得下, 但小数位无处表达
- {"DECIMAL(38,35)", true, types.FieldType_Float64, 0},
- // 其余新类型
- {"BOOLEAN", false, types.FieldType_Bool, 0},
- {"BOOL", true, types.FieldType_Bool, 0},
- {"YEAR", true, types.FieldType_Year, 0},
- {"UUID", true, types.FieldType_UUID, 0},
- // 二进制大对象 vs 文本: 这是本次新增的区分
- {"BLOB", false, types.FieldType_Blob, 0},
- {"TINYBLOB", true, types.FieldType_Blob, 0},
- {"MEDIUMBLOB", true, types.FieldType_Blob, 0},
- {"LONGBLOB", true, types.FieldType_Blob, 0},
- {"VARBINARY(64)", true, types.FieldType_Blob, 0},
- {"TEXT", false, types.FieldType_Dynamicbyte, 0},
- {"LONGTEXT", true, types.FieldType_Dynamicbyte, 0},
- // 定长串: 按字节容量【向上】取宽度档, 档位号写进列级参数 (Flag 低 5 位)。
- // 返回的 param 不再是 0 —— 它就是"这一列多宽"的载体 (见 types.FieldSize)。
- {"VARCHAR(8)", true, types.FieldType_String, types.WidthCodeFor(8)},
- {"VARCHAR(200)", true, types.FieldType_String, types.WidthCodeFor(200)},
- {"CHAR(32)", true, types.FieldType_String, types.WidthCodeFor(32)},
- // 超过旧的 256 字节天花板仍可以定长: 2048 以内都保得住"可索引"
- {"VARCHAR(1000)", true, types.FieldType_String, types.WidthCodeFor(1000)},
- // 只有超过 MaxInlineColumnWidth 才落变长列
- {"VARCHAR(4000)", true, types.FieldType_Dynamicbyte, 0},
- // 时间列落引擎的专用 datetime 类型 (Unix 毫秒), 不是动态列也不是普通整数
- {"DATETIME", true, types.FieldType_DateTime, 0},
- {"DATE", false, types.FieldType_DateTime, 0},
- {"TIMESTAMP", true, types.FieldType_DateTime, 0},
- }
- for _, c := range cases {
- ft, param := inferSQLType(c.decl, c.mysql)
- if ft != c.ft || param != c.param {
- t.Errorf("inferSQLType(%q, mysql=%v) = (%v, %d), 期望 (%v, %d)",
- c.decl, c.mysql, ft, param, c.ft, c.param)
- }
- }
- }
- // TestMySQLColumnDecl 钉住"字符列的容量按字节算"这条规则 (information_schema 里
- // CHARACTER_OCTET_LENGTH 那一列)。
- //
- // 这条是真实踩出来的: resource_info.resAlias 是 varchar(50) utf8mb4, 库里存了 73 字节
- // 的中文别名。老代码拿 50 (字符数) 去取档得到 64 字节, 整表迁移失败 —— 而 50 个字符在
- // utf8mb4 下最多是 200 字节, 只有拿 200 去取档才装得下。
- func TestMySQLColumnDecl(t *testing.T) {
- cases := []struct {
- name string
- dataType string
- // information_schema 给的三组长度: 字节容量 / 精度 / 小数位
- charBytes, numPrec, numScale int64
- want types.FieldType
- wantParam byte
- }{
- // varchar(50) utf8mb4 → octet 200 → 224 字节档 (老算法 50 → 64 字节 装不下中文;
- // 中间的过渡算法给 256 档, 现在能按声明精确到 224)
- {"utf8mb4 varchar(50)", "varchar", 200, 0, 0, types.FieldType_String, types.WidthCodeFor(200)},
- // varchar(12) utf8mb4 → octet 48 → 48 字节档
- {"utf8mb4 varchar(12)", "varchar", 48, 0, 0, types.FieldType_String, types.WidthCodeFor(48)},
- // 单字节字符集不背 4 倍: latin1 的 varchar(50) 就是 50 字节 (取 64 档)
- {"latin1 varchar(50)", "varchar", 50, 0, 0, types.FieldType_String, types.WidthCodeFor(50)},
- // 正好卡在旧天花板上: varchar(64) utf8mb4 = 256 字节
- {"utf8mb4 varchar(64)", "varchar", 256, 0, 0, types.FieldType_String, types.WidthCodeFor(256)},
- // 超过旧天花板不再落变长列: 320 字节仍在定长档内, 保住"可索引"
- {"utf8mb4 varchar(65)", "varchar", 260, 0, 0, types.FieldType_String, types.WidthCodeFor(260)},
- // 直到 MaxInlineColumnWidth (2048) 为止都还能定长
- {"utf8mb4 varchar(512)", "varchar", 2048, 0, 0, types.FieldType_String, types.WidthCodeFor(2048)},
- // 超出 2048 才落变长列 (变长只是没索引, 不会丢数据)
- {"utf8mb4 varchar(600)", "varchar", 2400, 0, 0, types.FieldType_Dynamicbyte, 0},
- {"utf8mb4 char(1)", "char", 4, 0, 0, types.FieldType_String, types.WidthCodeFor(4)},
- {"utf8mb4 text", "text", 65535, 0, 0, types.FieldType_Dynamicbyte, 0},
- {"blob", "blob", 65535, 0, 0, types.FieldType_Blob, 0},
- // 定点: (p,s) 要读进来, 小数位不能被抹平
- {"decimal(18,2)", "decimal", 20, 18, 2, types.FieldType_Decimal, 2},
- // 长度信息为 0 (数值/时间列没有 octet length) 时退回纯类型名
- {"int", "int", 0, 10, 0, types.FieldType_Int32, 0},
- {"datetime", "datetime", 0, 0, 0, types.FieldType_DateTime, 0},
- }
- for _, c := range cases {
- decl := mysqlColumnDecl(c.dataType, false, c.charBytes, c.numPrec, c.numScale)
- ft, param := inferSQLType(decl, true)
- if ft != c.want || param != c.wantParam {
- t.Errorf("%s: %q → (%v, %d), 期望 (%v, %d)",
- c.name, decl, ft, param, c.want, c.wantParam)
- }
- }
- }
- // TestMySQLUnsigned 无符号标志只能从 COLUMN_TYPE 读。
- //
- // MySQL 对 "bigint unsigned" 报的 DATA_TYPE 就是 "bigint" —— 只看 DATA_TYPE
- // 等于把无符号这件事整个丢掉, 于是无符号列全被当有符号收:
- // - BIGINT UNSIGNED 的 2^63 读回来是负数;
- // - INT UNSIGNED 的上界只剩一半。
- //
- // EXTRA 也靠不住 (只在显式 zerofill 时才有值), COLUMN_TYPE 两处都覆盖。
- func TestMySQLUnsigned(t *testing.T) {
- cases := []struct {
- columnType string
- want bool
- }{
- {"bigint(20) unsigned", true},
- {"int(10) unsigned zerofill", true}, // zerofill 隐含 unsigned
- {"bigint unsigned", true},
- {"BIGINT UNSIGNED", true}, // information_schema 大小写随版本
- {"bigint(20)", false},
- {"int(11)", false},
- {"varchar(50)", false},
- {"decimal(18,2)", false},
- {"datetime", false},
- }
- for _, c := range cases {
- if got := mysqlUnsigned(c.columnType); got != c.want {
- t.Errorf("mysqlUnsigned(%q) = %v, 期望 %v", c.columnType, got, c.want)
- }
- }
- }
- // TestMySQLBool BOOLEAN 只能从 COLUMN_TYPE 认出来。
- //
- // MySQL 的 BOOLEAN 就是 tinyint(1): information_schema 报的 DATA_TYPE 是
- // "tinyint"、COLUMN_TYPE 是 "tinyint(1)" —— 只看 DATA_TYPE 会走 TINYINT 分支
- // 收成 Int8, 而 sqltype.go 的 "BOOLEAN"/"BOOL" 分支对 MySQL 源永远不可达。
- // 宽度不是 1 的 tinyint 是真·小整数, tinyint(1) unsigned 能装 0..255,
- // 都不是真假语义, 不特判。
- func TestMySQLBool(t *testing.T) {
- cases := []struct {
- name string
- dataType, columnType string
- charBytes, numPrec, numScale int64
- want types.FieldType
- }{
- // BOOLEAN 列: DATA_TYPE 只报 tinyint, 靠 COLUMN_TYPE 特判成 Bool
- {"boolean", "tinyint", "tinyint(1)", 0, 3, 0, types.FieldType_Bool},
- // 宽度不是 1 的 tinyint 是真·小整数
- {"tinyint(4)", "tinyint", "tinyint(4)", 0, 3, 0, types.FieldType_Int8},
- // tinyint(1) unsigned 能装 0..255, 不是真假语义
- {"tinyint(1) unsigned", "tinyint", "tinyint(1) unsigned", 0, 3, 0, types.FieldType_Uint8},
- }
- for _, c := range cases {
- decl := mysqlColumnDecl(c.dataType, mysqlUnsigned(c.columnType), c.charBytes, c.numPrec, c.numScale)
- if mysqlBool(c.columnType) {
- decl = "boolean"
- }
- ft, _ := inferSQLType(decl, true)
- if ft != c.want {
- t.Errorf("%s: COLUMN_TYPE=%q → decl %q → %v, 期望 %v",
- c.name, c.columnType, decl, ft, c.want)
- }
- }
- }
- // TestMySQLColumnDeclUnsigned 无符号后缀必须真的走到 inferSQLType,
- // 而且不能污染第一个括号里的容量 / 精度参数。
- func TestMySQLColumnDeclUnsigned(t *testing.T) {
- cases := []struct {
- name string
- dataType, columnType string
- charBytes, numPrec, numScale int64
- want types.FieldType
- wantParam byte
- }{
- // DATA_TYPE 丢了 unsigned, 补上 COLUMN_TYPE 才能收成无符号档
- {"bigint unsigned", "bigint", "bigint(20) unsigned", 0, 20, 0, types.FieldType_Uint64, 0},
- {"bigint signed", "bigint", "bigint(20)", 0, 20, 0, types.FieldType_Int64, 0},
- {"int unsigned", "int", "int(10) unsigned", 0, 10, 0, types.FieldType_Uint32, 0},
- {"int signed", "int", "int(11)", 0, 10, 0, types.FieldType_Int32, 0},
- {"smallint unsigned", "smallint", "smallint(5) unsigned", 0, 5, 0, types.FieldType_Uint16, 0},
- {"tinyint unsigned", "tinyint", "tinyint(3) unsigned", 0, 3, 0, types.FieldType_Uint8, 0},
- // 后缀在最后, 不能把 (n) / (p,s) 吃掉
- {"varchar(50) utf8mb4 unsigned", "varchar", "varchar(50) unsigned", 200, 0, 0,
- types.FieldType_String, types.WidthCodeFor(200)},
- {"decimal(18,2) unsigned", "decimal", "decimal(18,2) unsigned", 0, 18, 2,
- types.FieldType_Decimal, 2},
- }
- for _, c := range cases {
- decl := mysqlColumnDecl(c.dataType, mysqlUnsigned(c.columnType), c.charBytes, c.numPrec, c.numScale)
- ft, param := inferSQLType(decl, true)
- if ft != c.want || param != c.wantParam {
- t.Errorf("%s: COLUMN_TYPE=%q → decl %q → (%v, %d), 期望 (%v, %d)",
- c.name, c.columnType, decl, ft, param, c.want, c.wantParam)
- }
- }
- }
- // TestSQLTypeRoundTrip 声明出去的类型名必须能被自己读回同一个类型。
- //
- // 例外的几种都【不依赖 meta 就得靠宽度/语义兜底】, 这里显式列出来当文档:
- // - 窄整数在 SQLite 只有 INTEGER 一种写法, 读回一律 Int64;
- // - Decimal 在 SQLite 没有原生类型 (落 TEXT), 读回是 Dynamicbyte;
- // - UUID 在 MySQL 用 BINARY(16) 存, 读回被认成 Blob;
- // - Blob 与 Dynamicbyte 盘上同为动态列, 靠 meta 才能区分到具体哪一种;
- // - 定长串的**宽度码不是逐位往返的**: 声明成 VARCHAR(列宽) 再读回来会按宽度重新取档,
- // 档位相同, 但"用类型默认档 (param 0)"的列会变成"写明档位 (param≠0)"。
- // 所以定长串钉的不变量是【字节宽度】(见下面第二个循环), 不是 param 字面值。
- //
- // 也就是说: 需要"精确类型"的往返都依赖 __vrtx_meta (默认开启); 没有 meta 时
- // 至少保证["数据不丢、语义不离谱"]。
- func TestSQLTypeRoundTrip(t *testing.T) {
- type rt struct {
- ft types.FieldType
- param byte
- }
- for _, mysql := range []bool{true, false} {
- // 这些类型在两个方言下都能原样往返
- exact := []rt{
- {types.FieldType_Blob, 0},
- {types.FieldType_Bool, 0},
- {types.FieldType_Year, 0},
- {types.FieldType_Float64, 0},
- {types.FieldType_Int64, 0},
- {types.FieldType_DateTime, 0}, // 无 TimeKind 时按 DATETIME 声明导出再读回
- }
- if mysql {
- exact = append(exact,
- rt{types.FieldType_Int8, 0},
- rt{types.FieldType_Int16, 0},
- rt{types.FieldType_Int32, 0},
- rt{types.FieldType_Uint8, 0},
- rt{types.FieldType_Uint16, 0},
- rt{types.FieldType_Uint32, 0},
- rt{types.FieldType_Uint64, 0},
- rt{types.FieldType_Float32, 0},
- rt{types.FieldType_Decimal, 2},
- )
- }
- // 定长串: 往返不变量是【字节宽度】。
- //
- // 声明成 VARCHAR(列宽) 之后再读回来会重新算宽度码: 宽度码列往返后宽度不变;
- // 用类型默认宽度 (param 0) 的列则会拿到一个显式宽度码 —— 宽度一样, 只是表示法
- // 从"默认宽度"变成"写明档位"。这正是本节要钉住的性质: 变的只是表示, 不是行内布局。
- stringCases := []rt{{types.FieldType_String, 0}}
- for _, size := range []int{4, 16, 32, 48, 100, 200, 256, 300, 1000, 2048} {
- ft, param := bucketColumn(size)
- stringCases = append(stringCases, rt{ft, param})
- }
- for _, w := range stringCases {
- decl := sqlTypeName(w.ft, w.param, mysql)
- ft, param := inferSQLType(decl, mysql)
- if ft != w.ft || types.FieldSize(ft, param) != types.FieldSize(w.ft, w.param) {
- t.Errorf("mysql=%v: %v/param%d → %q → (%v, param%d): 类型或行内宽度变了 (%d → %d 字节)",
- mysql, w.ft, w.param, decl, ft, param,
- types.FieldSize(w.ft, w.param), types.FieldSize(ft, param))
- }
- }
- for _, e := range exact {
- decl := sqlTypeName(e.ft, e.param, mysql)
- ft, param := inferSQLType(decl, mysql)
- if ft != e.ft || param != e.param {
- t.Errorf("mysql=%v: %v → %q → (%v, %d), 期望 (%v, %d)",
- mysql, e.ft, decl, ft, param, e.ft, e.param)
- }
- }
- }
- }
- // TestDeclaredCapacityNote 声明容量超过单值上限的列必须【报出来】, 且不许误报。
- //
- // 落动态列不等于"声明多大都行": 动态列的内容整体落在一个堆页里, 单值上限是
- // types.MaxStringSize。源库里 varchar(60000) 这种声明在 VRTX 端兑现不了, 而读端是唯一看得到
- // 声明原文的地方 —— 在这里漏掉, 用户只会看到一条"容量比声明小得多"的列而没有任何提示。
- //
- // 反过来误报同样有害: TEXT / BLOB 家族在 MySQL 的 information_schema 里带着【类型自身的】
- // 上界 (text → CHARACTER_OCTET_LENGTH = 65535), 那个数不是用户的声明, 按它报"兑现不了"会让
- // 每一张带 TEXT 列的表都挂上一条无意义的警告, 真正的容量问题反而被淹掉。
- //
- // 容量报告只影响提示, 不影响类型: 同一个入口推出来的类型必须与 inferSQLType 逐字节一致,
- // 否则"报了一句"就悄悄改了列的类型, 比不报更糟。
- func TestDeclaredCapacityNote(t *testing.T) {
- atCap := fmt.Sprintf("VARCHAR(%d)", types.MaxStringSize)
- overCap := fmt.Sprintf("VARCHAR(%d)", types.MaxStringSize+1)
- cases := []struct {
- decl string
- mysql bool
- ft types.FieldType
- param byte
- note bool // 是否应当报"容量封顶"
- }{
- // 还在定长档: 容量本来就远小于单值上限, 不报
- {"VARCHAR(2048)", true, types.FieldType_String, types.WidthCodeFor(2048), false},
- // 已落动态列但声明没超限: 声明多少就能装多少, 不报
- {atCap, false, types.FieldType_Dynamicbyte, 0, false},
- // 超过一个字节就兑现不了: 报
- {overCap, false, types.FieldType_Dynamicbyte, 0, true},
- {"VARCHAR(60000)", true, types.FieldType_Dynamicbyte, 0, true},
- // BINARY / VARBINARY 落 blob 列 (行内 8 字节引用 + 可跨页的链, 上限约 4GB):
- // 声明多大的容量都兑现得了, 报"封顶"就是假消息。
- {"BINARY(16)", true, types.FieldType_Blob, 0, false},
- {"VARBINARY(60000)", true, types.FieldType_Blob, 0, false},
- // TEXT / BLOB 家族: 括号里那个数是类型自身的上界, 不是用户的声明 → 不报
- {"TEXT(65535)", true, types.FieldType_Dynamicbyte, 0, false},
- {"LONGTEXT(4294967295)", true, types.FieldType_Dynamicbyte, 0, false},
- {"LONGBLOB(4294967295)", true, types.FieldType_Blob, 0, false},
- // 其它带括号的类型: 括号里是精度 / 小数位, 与容量无关
- {"DECIMAL(18,2)", true, types.FieldType_Decimal, 2, false},
- }
- for _, c := range cases {
- ft, param, note := inferSQLTypeWithNote(c.decl, c.mysql)
- if ft != c.ft || param != c.param {
- t.Errorf("inferSQLTypeWithNote(%q, mysql=%v) = (%v, %d), 期望 (%v, %d)",
- c.decl, c.mysql, ft, param, c.ft, c.param)
- }
- if got := note != ""; got != c.note {
- t.Errorf("%q: 报了容量=%v (%q), 期望 =%v", c.decl, got, note, c.note)
- }
- }
- // 报出来的话要能直接定位问题: 用户写下的数与引擎能给的上限, 两个都得有。
- _, _, note := inferSQLTypeWithNote(overCap, false)
- for _, want := range []string{strconv.Itoa(types.MaxStringSize + 1), strconv.Itoa(types.MaxStringSize)} {
- if !strings.Contains(note, want) {
- t.Errorf("容量报告里缺少 %q: %q", want, note)
- }
- }
- }
|