|
|
@@ -1,27 +1,37 @@
|
|
|
package migrate
|
|
|
|
|
|
-// ============================ TEXT 列采样定宽 ============================
|
|
|
+// ============================ 文本列采样定宽 / 字符列超档升档 ============================
|
|
|
//
|
|
|
-// 【问题】inferSQLType 把 TEXT 家族 (TEXT/TINYTEXT/MEDIUMTEXT/LONGTEXT/CLOB/NCLOB)
|
|
|
+// 【问题 1】inferSQLType 把 TEXT 家族 (TEXT/TINYTEXT/MEDIUMTEXT/LONGTEXT/CLOB/NCLOB)
|
|
|
// 一律落成 FieldType_Dynamicbyte: 动态列不能建索引 (引擎直接拒, 见 vrtx.go 建索引
|
|
|
// 那段的 ErrIndexDynamicField 处理), 读要走堆 —— 而真实库里很多 TEXT 列装的就是
|
|
|
// 几十几百字节的短文本 (备注 / 状态 / 标签), 白白失去索引能力。
|
|
|
//
|
|
|
+// 【问题 2】带声明长度的 CHAR/VARCHAR(n) 按 bucketColumn 取定长档, 但 SQLite 不强制
|
|
|
+// 声明长度 (MySQL 强制, 数据不可能超档) —— DockerManager 实测: sqlite 的 sys_ssl.cert
|
|
|
+// 声明 VARCHAR(2048), 实际值 4803 字节, 按声明建 2048 定长档后写入即报"值 4803 字节
|
|
|
+// 超过定长列容量 2048"。
|
|
|
+//
|
|
|
// 【处理】Options.TextWidthScan (默认开) 让迁移在建目标表之前先【采样】: 对源表跑一次
|
|
|
-// SELECT MAX(LENGTH(col)) (同表多列合一句, 每表一次), 按采样到的实际最大长度三态分流:
|
|
|
-// 不超过 types.MaxInlineColumnWidth (2048) 的向上取 2 的幂 (下限 textWidthMin) 落成
|
|
|
-// 定长 VARCHAR 档 (可索引、读不走堆); 超过 2048 但不超过单值上限 types.MaxStringSize
|
|
|
-// (16360) 的保持动态列; 超过单值上限的落 Blob 链式列 (行内 8B 引用 + 链页, 上限约
|
|
|
-// 4GB) —— 动态列按单块上限拒写, 保持动态列会在【写入时】整表失败, 落 Blob 才能迁得动。
|
|
|
+// SELECT MAX(LENGTH(col)) (两路候选 —— TEXT 家族动态列 + 声明长度字符列 —— 同表合
|
|
|
+// 一句, 每表一次), 按采样到的实际最大长度三态分流:
|
|
|
+// - 不超过 types.MaxInlineColumnWidth (2048) 的向上取 2 的幂 (下限 textWidthMin)
|
|
|
+// 落成定长 VARCHAR 档 (可索引、读不走堆);
|
|
|
+// - 超过 2048 但不超过单值上限 types.MaxStringSize (16360) 的保持/升为动态列;
|
|
|
+// - 超过单值上限的落 Blob 链式列 (行内 8B 引用 + 链页, 上限约 4GB) —— 动态列按
|
|
|
+// 单块上限拒写, 保持动态列会在【写入时】整表失败, 落 Blob 才能迁得动。
|
|
|
+//
|
|
|
+// 声明长度字符列 (declaredCharWidth 那一路) 多一层"当前档宽"对照: 采样值没超当前
|
|
|
+// 档宽就不动 (声明兑现), 超了才按上面三态升档 (见 scanCharWidthUpgrade)。
|
|
|
//
|
|
|
// 【权衡】采样反映的是【当下】的数据: 定宽列会约束未来写入 (超宽即报错), 2 次幂圆整
|
|
|
// 最多浪费一倍行内空间; 且采样按表做一次, 行数大时是一次额外的全表扫描。在意这些就
|
|
|
-// 关掉开关, 退回"TEXT 全落动态列"的旧行为。
|
|
|
+// 关掉开关, 退回"TEXT 全落动态列 / 字符列按声明取档"的旧行为。
|
|
|
//
|
|
|
// 【为什么判定看源声明 (Column.Decl) 而不是看目标列类型】目标是动态列的不止 TEXT:
|
|
|
-// JSON / TIME / 未知的兜底类型也都落动态列, 而它们不该被采样改宽 —— 采样定宽只对
|
|
|
-// "源库明确说了这是无长度文本"的列生效。带长度的 CHAR/VARCHAR 按声明容量取档
|
|
|
-// (bucketColumn), 也不经过这里。
|
|
|
+// JSON / TIME / 未知的兜底类型也都落动态列, 而它们不该被采样改宽 —— 采样只对"源库
|
|
|
+// 明确说了这是文本"的列生效: TEXT 家族看 isTextFamilyDecl, 声明长度字符列看
|
|
|
+// declaredCharWidth, 两者都从声明原文判定。
|
|
|
//
|
|
|
// 【为什么用可选接口而不是给 Source 加方法】采样需要 SQL 查询通道, 只有 *SQL 有;
|
|
|
// VRTX 源 (VRTX → VRTX 拷贝) 没有这一层, 也不会声明 TEXT 列。给 Source 接口加方法
|
|
|
@@ -53,11 +63,12 @@ type TextWidthSampler interface {
|
|
|
const textWidthMin = 32
|
|
|
|
|
|
// isTextFamilyDecl 源声明是否 TEXT 家族 (TEXT/TINYTEXT/MEDIUMTEXT/LONGTEXT/CLOB/NCLOB)。
|
|
|
-// 用 splitTypeArgs 取基类型名: JSON / BLOB / BINARY / VARBINARY 与带长度的 CHAR /
|
|
|
-// VARCHAR 都不在里面 —— 它们各有各的归宿 (JSON 保持动态列, 二进制落 Blob 类型,
|
|
|
-// 声明长度的字符列按声明容量取档), 不参与采样。带长度的 "TEXT(65535)" 声明
|
|
|
-// (MySQL 的 information_schema 会给 TEXT 报一个类型自身上界) 同样算 TEXT 家族 —
|
|
|
-// 那是类型的上界, 不是用户声明的宽度。
|
|
|
+// 用 splitTypeArgs 取基类型名: JSON / BLOB / BINARY / VARBINARY 都不在里面 —— 它们
|
|
|
+// 各有各的归宿 (JSON 保持动态列, 二进制落 Blob 类型), 不参与采样。带长度的
|
|
|
+// "TEXT(65535)" 声明 (MySQL 的 information_schema 会给 TEXT 报一个类型自身上界)
|
|
|
+// 同样算 TEXT 家族 —— 那是类型的上界, 不是用户声明的宽度。带长度的 CHAR/VARCHAR
|
|
|
+// 也不在这里: 那一路由 declaredCharWidth 单独判定 (取档规则不同, 见
|
|
|
+// scanCharWidthUpgrade)。
|
|
|
func isTextFamilyDecl(decl string) bool {
|
|
|
base, _ := splitTypeArgs(strings.ToUpper(strings.TrimSpace(decl)))
|
|
|
switch base {
|
|
|
@@ -67,6 +78,25 @@ func isTextFamilyDecl(decl string) bool {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
+// declaredCharWidth 源声明为【带长度的字符列】时返回声明字节数 args[0], 其余声明
|
|
|
+// (无长度 / 非 CHAR/VARCHAR 家族) 返回 0。基类型名单与 inferSQLType 的字符列分支
|
|
|
+// 同源: 这族声明按 bucketColumn 取定长档, 但 SQLite 不强制声明长度, 实际数据可能
|
|
|
+// 超档 (DockerManager 实测 sys_ssl.cert VARCHAR(2048) 存 4803 字节) —— 声明字节数
|
|
|
+// 就是"承诺兑现"的对照线, 采样超了它升档 (见 scanCharWidthUpgrade)。MySQL 侧读的
|
|
|
+// 是 CHARACTER_OCTET_LENGTH (字节数), 同一函数兼容两种来源。
|
|
|
+func declaredCharWidth(decl string) int {
|
|
|
+ base, args := splitTypeArgs(strings.ToUpper(strings.TrimSpace(decl)))
|
|
|
+ switch base {
|
|
|
+ case "CHAR", "CHARACTER", "VARCHAR", "NCHAR", "NVARCHAR", "VARCHAR2":
|
|
|
+ default:
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ if len(args) == 0 {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return args[0]
|
|
|
+}
|
|
|
+
|
|
|
// textWidthClass 采样最大长度的三态归宿。
|
|
|
type textWidthClass int
|
|
|
|
|
|
@@ -104,28 +134,31 @@ func textWidthCode(maxLen int64) (code byte, width int, class textWidthClass) {
|
|
|
return types.WidthCodeFor(w), w, widthToFixed
|
|
|
}
|
|
|
|
|
|
-// scanTextWidths TEXT 采样定宽主逻辑, 建目标表之前调用: 找出 schema 里"源声明
|
|
|
-// TEXT 家族、当前推断为动态列"的列, 让源端按表采样一次, 按三态归宿改类型 (见
|
|
|
-// textWidthCode): 够窄的改落 FieldType_String + 宽度码 (可索引), 中间的保持动态列,
|
|
|
-// 超过动态列单值上限的落 FieldType_Blob (链式存储, 否则写入时必败)。表结构就地修改,
|
|
|
-// 返回写进迁移报告的说明 (无命中列时返回 "")。
|
|
|
+// scanTextWidths 宽度采样主逻辑, 建目标表之前调用。两路候选 (见
|
|
|
+// isTextWidthCandidate) 合进同一次 per-table 采样:
|
|
|
+//
|
|
|
+// - TEXT 家族动态列: 按三态归宿改类型 (见 textWidthCode) —— 够窄的改落
|
|
|
+// FieldType_String + 宽度码 (可索引), 中间的保持动态列, 超过动态列单值上限的落
|
|
|
+// FieldType_Blob (链式存储, 否则写入时必败);
|
|
|
+// - 声明长度字符列: 实际数据超当前档宽的按同一套三态升档 (见 scanCharWidthUpgrade),
|
|
|
+// 没超档宽不动 (声明兑现)。
|
|
|
+//
|
|
|
+// 表结构就地修改, 返回写进迁移报告的说明 (无命中列时返回 "")。
|
|
|
//
|
|
|
// 放在 prepareForVRTX / fitRowWidthForVRTX 【之前】: 原主键列退化成普通列时要不要
|
|
|
// 补索引 (ensureColumnIndex 只给非动态列补), 以及行宽闸门要不要把采样得过宽的列
|
|
|
// 再降回动态列, 都该看到采样后的最终列宽。
|
|
|
//
|
|
|
-// 采样失败不判死: 记日志并退回旧行为 (全落动态列), 由返回值在报告里说明 —— 采样
|
|
|
+// 采样失败不判死: 记日志并退回旧行为 (保持推断类型), 由返回值在报告里说明 —— 采样
|
|
|
// 是"让列更好用"的优化, 不该把以前能迁的表拦下来。
|
|
|
func scanTextWidths(src Source, sc *TableSchema, o *Options) string {
|
|
|
var cols []string
|
|
|
for i := range sc.Columns {
|
|
|
- c := &sc.Columns[i]
|
|
|
- if c.Type != types.FieldType_Dynamicbyte || !isTextFamilyDecl(c.Decl) {
|
|
|
- continue
|
|
|
+ if isTextWidthCandidate(&sc.Columns[i]) {
|
|
|
+ cols = append(cols, sc.Columns[i].Name)
|
|
|
}
|
|
|
- cols = append(cols, c.Name)
|
|
|
}
|
|
|
- // 不能提前 return: NUMERIC 域探测 (scanNumericDomain) 在 TEXT 列缺席时仍要跑。
|
|
|
+ // 不能提前 return: NUMERIC 域探测 (scanNumericDomain) 在采样列缺席时仍要跑。
|
|
|
if len(cols) == 0 {
|
|
|
return scanNumericDomain(src, sc, o)
|
|
|
}
|
|
|
@@ -135,16 +168,20 @@ func scanTextWidths(src Source, sc *TableSchema, o *Options) string {
|
|
|
}
|
|
|
maxLens, err := sampler.TextWidths(sc.Name, cols)
|
|
|
if err != nil {
|
|
|
- o.logf("[migrate] 表 %s 的 TEXT 采样失败, 保持动态列 (旧行为): %v", sc.Name, err)
|
|
|
- return appendNote(fmt.Sprintf("text 采样失败保持动态列 (%v)", err), scanNumericDomain(src, sc, o))
|
|
|
+ o.logf("[migrate] 表 %s 的宽度采样失败, 保持推断类型 (旧行为): %v", sc.Name, err)
|
|
|
+ return appendNote(fmt.Sprintf("text 采样失败保持推断类型 (%v)", err), scanNumericDomain(src, sc, o))
|
|
|
}
|
|
|
- var fixed, kept, blobbed []string
|
|
|
+ var fixed, kept, blobbed, upgraded []string
|
|
|
for i := range sc.Columns {
|
|
|
c := &sc.Columns[i]
|
|
|
+ maxLen := maxLens[c.Name] // 缺项视为 0, 全 NULL 的列落最窄档
|
|
|
if c.Type != types.FieldType_Dynamicbyte || !isTextFamilyDecl(c.Decl) {
|
|
|
+ // TEXT 家族以外走字符列那一路 (非候选列在里面自行排除)。
|
|
|
+ if up := scanCharWidthUpgrade(c, maxLen, o); up != "" {
|
|
|
+ upgraded = append(upgraded, up)
|
|
|
+ }
|
|
|
continue
|
|
|
}
|
|
|
- maxLen := maxLens[c.Name] // 缺项视为 0, 全 NULL 的列落最窄档
|
|
|
code, width, class := textWidthCode(maxLen)
|
|
|
switch class {
|
|
|
case widthToBlob:
|
|
|
@@ -178,9 +215,73 @@ func scanTextWidths(src Source, sc *TableSchema, o *Options) string {
|
|
|
note = appendNote(note, fmt.Sprintf("text 列超 %d 字节落 Blob 链式列 (%s)",
|
|
|
types.MaxStringSize, strings.Join(blobbed, ", ")))
|
|
|
}
|
|
|
+ if len(upgraded) > 0 {
|
|
|
+ note = appendNote(note, "声明长度字符列超档升档 ("+strings.Join(upgraded, "; ")+")")
|
|
|
+ }
|
|
|
return appendNote(note, scanNumericDomain(src, sc, o))
|
|
|
}
|
|
|
|
|
|
+// isTextWidthCandidate 这一列是否参与建表前的宽度采样。两路与源声明一一对应:
|
|
|
+// - TEXT 家族动态列 (isTextFamilyDecl): 采样实际最大长度, 够窄的从动态列【升】定长档;
|
|
|
+// - 声明长度字符列 (declaredCharWidth): 定长档装不下实际数据时【升档】兜住。
|
|
|
+//
|
|
|
+// 其余列各有各的归宿, 不碰: JSON / TIME / 未知兜底类型保持动态列, 二进制落 Blob 类型
|
|
|
+// (声明多大的容量都兑现得了, v10 起 Blob 行内是 8B 链引用、单值约 4GB), 无长度字符
|
|
|
+// 声明按 32 字节档处理 —— 它们都没有"声明了宽度却装不下"的落差。
|
|
|
+func isTextWidthCandidate(c *Column) bool {
|
|
|
+ if c.Type == types.FieldType_Dynamicbyte && isTextFamilyDecl(c.Decl) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ // 声明长度字符列: 定长档 (FieldType_String) 的核对档宽; 声明超 2048 落成动态列
|
|
|
+ // 的也要 —— 那种列只剩"超单值上限落 Blob"一种升档, 不采样就会在写入时整表失败
|
|
|
+ // (与 TEXT 家族超单值上限落 Blob 是同一个坑)。
|
|
|
+ return declaredCharWidth(c.Decl) > 0 &&
|
|
|
+ (c.Type == types.FieldType_String || c.Type == types.FieldType_Dynamicbyte)
|
|
|
+}
|
|
|
+
|
|
|
+// scanCharWidthUpgrade 声明长度字符列的【超档升档】: 采样 maxLen 没超当前档宽就不动
|
|
|
+// (声明兑现; MySQL 强制声明长度, 数据不可能超档, 恒走这条, 所以这路对 MySQL 无害);
|
|
|
+// 超了按 textWidthCode 三态升档 —— 能定长则升 2 次幂定长档 (上限 2048), 超 2048 落
|
|
|
+// 动态列, 超 types.MaxStringSize (16360) 落 Blob 链式列 (动态列按单块上限拒写, 不升
|
|
|
+// Blob 这类大值会在写入时让整表迁移失败)。就地修改列类型, 返回写进迁移报告的说明
|
|
|
+// (无升档时返回 "")。
|
|
|
+func scanCharWidthUpgrade(c *Column, maxLen int64, o *Options) string {
|
|
|
+ declared := declaredCharWidth(c.Decl)
|
|
|
+ if declared <= 0 ||
|
|
|
+ (c.Type != types.FieldType_String && c.Type != types.FieldType_Dynamicbyte) {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ // 当前档宽: 定长列看宽度码对应的档位; 已是动态列的按单值上限算 (动态列的"档"
|
|
|
+ // 就是整个堆页, 装得下任何不超单值上限的值) —— 那种列只剩超单值上限落 Blob
|
|
|
+ // 这一种升档, textWidthCode 自然会给出。
|
|
|
+ curWidth := types.MaxStringSize
|
|
|
+ if c.Type == types.FieldType_String {
|
|
|
+ curWidth = int(types.FieldSize(c.Type, c.Param))
|
|
|
+ }
|
|
|
+ if maxLen <= int64(curWidth) {
|
|
|
+ return "" // 声明兑现: 实际数据装得进当前档, 不动。
|
|
|
+ }
|
|
|
+ code, width, class := textWidthCode(maxLen)
|
|
|
+ var target string
|
|
|
+ switch class {
|
|
|
+ case widthToBlob:
|
|
|
+ c.Type = types.FieldType_Blob
|
|
|
+ c.Param = 0
|
|
|
+ target = "Blob 链式列"
|
|
|
+ case widthToDynamic:
|
|
|
+ c.Type = types.FieldType_Dynamicbyte
|
|
|
+ c.Param = 0
|
|
|
+ target = "动态列"
|
|
|
+ default:
|
|
|
+ c.Type = types.FieldType_String
|
|
|
+ c.Param = code
|
|
|
+ target = fmt.Sprintf("%d 字节档", width)
|
|
|
+ }
|
|
|
+ o.vlogf("[migrate] 字符列 %s 声明 %d 字节但采样最大 %d 字节, 升档 %s",
|
|
|
+ c.Name, declared, maxLen, target)
|
|
|
+ return fmt.Sprintf("列 %s 声明 %d 字节但实际最大 %d 字节, 升档 %s", c.Name, declared, maxLen, target)
|
|
|
+}
|
|
|
+
|
|
|
// isBareNumericDecl 源声明是否【裸 NUMERIC】(基类型 NUMERIC 且无 (p,s) 参数)。
|
|
|
// 只有这种声明会被 inferSQLType (SQLite 分支) 收成 Bool; 它同时是数据探测的
|
|
|
// 目标 — 真定点列要么带精度 NUMERIC(p,s), 要么写 DECIMAL, 都不进这里。
|