Quellcode durchsuchen

fix(migrate,web): MySQL tinyint(1) 映射 Bool + 批量参数上限

- mysql: COLUMN_TYPE=tinyint(1) 特判为 Bool (原 BOOLEAN case 因 DATA_TYPE
  实际返回 tinyint 永远不可达); tinyint(1) unsigned 仍是 Uint8
- vrtxsqlweb/vrtxsql: batch 上限 50000, 超出返回 400/报错, 防预分配 OOM
yl32940 vor 3 Wochen
Ursprung
Commit
054c8b6299
4 geänderte Dateien mit 64 neuen und 0 gelöschten Zeilen
  1. 4 0
      cmd/vrtxsql/main.go
  2. 9 0
      cmd/vrtxsqlweb/main.go
  3. 17 0
      migrate/mysql.go
  4. 34 0
      migrate/sqltype_test.go

+ 4 - 0
cmd/vrtxsql/main.go

@@ -89,6 +89,10 @@ func run(args []string) error {
 	if c.from == c.to {
 		return fmt.Errorf("-from 与 -to 是同一个库")
 	}
+	const maxBatchRows = 50000 // 与 vrtxsqlweb 一致: 防 make([]Row,0,Batch) 预分配 OOM
+	if c.batch > maxBatchRows {
+		return fmt.Errorf("-batch 超过上限 %d", maxBatchRows)
+	}
 
 	opts := buildOptions(c)
 	src, err := migrate.ParseDSN(c.from, opts)

+ 9 - 0
cmd/vrtxsqlweb/main.go

@@ -450,6 +450,11 @@ type migrateReq struct {
 	EachRow bool `json:"eachRow"`
 }
 
+// maxBatchRows 批量接口一次请求允许的最大批大小。batch 直接控制 migrate 包
+// make([]Row, 0, Batch) 的预分配 (见 migrate/run.go), 不设上限一次请求就能
+// 预分配出超大切片把服务打 OOM。
+const maxBatchRows = 50000
+
 // streamEvent 迁移过程以 NDJSON 一行一条推送 (前端按行解析):
 //
 //	{"type":"log",      "at":"15:04:05","msg":"...","level":"stage"}
@@ -493,6 +498,10 @@ func handleMigrate(w http.ResponseWriter, r *http.Request) {
 		http.Error(w, "请求体解析失败: "+err.Error(), http.StatusBadRequest)
 		return
 	}
+	if req.Batch > maxBatchRows {
+		http.Error(w, fmt.Sprintf("batch 超过上限 %d", maxBatchRows), http.StatusBadRequest)
+		return
+	}
 
 	flusher, _ := w.(http.Flusher)
 	w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")

+ 17 - 0
migrate/mysql.go

@@ -175,6 +175,17 @@ func mysqlUnsigned(columnType string) bool {
 	return strings.Contains(strings.ToLower(columnType), "unsigned")
 }
 
+// mysqlBool 判断 COLUMN_TYPE 是不是 BOOLEAN 列。
+//
+// 与 unsigned 同理, 这件事 DATA_TYPE 看不出来: MySQL 的 BOOLEAN 就是 tinyint(1),
+// information_schema 报的 DATA_TYPE 是 "tinyint"、COLUMN_TYPE 是 "tinyint(1)" ——
+// 只看 DATA_TYPE 会走 inferSQLType 的 TINYINT 分支收成 Int8, 而 sqltype.go 的
+// "BOOLEAN"/"BOOL" 分支对 MySQL 源永远不可达。宽度不是 1 的 tinyint 是真·小整数,
+// 仍按 Int8 收; tinyint(1) unsigned 能装 0..255, 也不是真假语义, 不特判。
+func mysqlBool(columnType string) bool {
+	return strings.EqualFold(strings.TrimSpace(columnType), "tinyint(1)")
+}
+
 // columns 读列 + 单列主键名。
 //
 // 主键的那点信息这里最全, 所以两件"VRTX 承载不了"的事在这里记下 (见 sourceNotes):
@@ -205,6 +216,12 @@ func (d mysqlDial) columns(db *sql.DB, table string) ([]Column, string, sourceNo
 			return nil, "", sourceNotes{}, err
 		}
 		decl := mysqlColumnDecl(dataType, mysqlUnsigned(columnType), charBytes, numPrec, numScale)
+		if mysqlBool(columnType) {
+			// MySQL 的 BOOLEAN 就是 tinyint(1), DATA_TYPE 只报 "tinyint": 不特判只会
+			// 落到 Int8, 等值/索引给出的是整数语义而不是真假。换成 boolean 走
+			// inferSQLType 的 BOOL 分支, 先于常规 tinyint→Int8 路径生效。
+			decl = "boolean"
+		}
 		ft, param, note := inferSQLTypeWithNote(decl, true)
 		if note != "" {
 			notes.Capped = append(notes.Capped, name+": "+note)

+ 34 - 0
migrate/sqltype_test.go

@@ -174,6 +174,40 @@ func TestMySQLUnsigned(t *testing.T) {
 	}
 }
 
+// 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) {