package v8 import ( "context" "database/sql" "dbview/service/internal/common/databases/meta" "encoding/json" "fmt" "reflect" "sort" "strings" "time" ) // GetRootObjects 获取MySQL实例下的所有数据库(根对象) // connInfo 仅用于错误提示/上下文,不用于连接建立(连接由外部 db 实例提供) func (q *MySQLDriver) GetRootObjects(ctx context.Context, connID string, connInfo string, page, pageSize int, fetch bool) ([]meta.GenericObject, int64, error) { db := q.db // 2. 查询总数(分页用) var total int64 if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA").Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询数据库实例下的 schema 总数失败(INFORMATION_SCHEMA.SCHEMATA):%w", err) } // 3. 分页查询库列表 offset := (page - 1) * pageSize // 某些 MySQL 版本或驱动对在 LIMIT/OFFSET 中使用占位符兼容性不好,改为直接插入整数常量(page,pageSize 为内部控制,非外部注入) query := fmt.Sprintf( "SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA LIMIT %d OFFSET %d", pageSize, offset) // 如果不需要取具体数据(fetch==false),只返回总数和 root 的 type 信息(前端可用来决定后续请求) if !fetch { // root 层的类型通常为 database // 为类型占位对象添加 Attrs,包含前端可用的请求模板(占位符形式);Description 已移到结构体字段 types := []meta.GenericObject{{ ID: fmt.Sprintf("%s.type-database", connID), Name: "database", Description: "数据库(root)类型,占位用于导航;前端可据此请求数据库下的子类型/条目", Type: "database", ParentID: connInfo, DBType: "mysql", Attrs: map[string]string{}, }} return types, total, nil } rows, err := db.QueryContext(ctx, query) if err != nil { return nil, 0, fmt.Errorf("分页查询 schema 列表失败(conn=%s,page=%d,pageSize=%d, sql=%s):%w", connInfo, page, pageSize, query, err) } defer rows.Close() // 4. 映射为GenericObject(根对象类型为"database") var objects []meta.GenericObject for rows.Next() { var dbName sql.NullString var charset sql.NullString if err := rows.Scan(&dbName, &charset); err != nil { // 忽略该行并继续 continue } objects = append(objects, meta.GenericObject{ ID: fmt.Sprintf("%s.db-%s", connID, dbName.String), // ID格式:connID.db-库名 Name: dbName.String, Type: "database", // 根对象类型为数据库 ParentID: connInfo, // 父对象为实例(connInfo) DBType: "mysql", Attrs: map[string]string{ "charset": charset.String, // MySQL库特有属性:默认字符集 "createTime": "", }, Children: []meta.GenericObject{}, // 子对象暂不加载 }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历 schema 结果集出错:%w", err) } return objects, total, nil } // GetChildObjects 获取父对象下的子对象(使用结构化路径定位父对象) // path: 从根到父对象的完整路径(见 meta.ObjectPath),最后一项为父对象自身 // childType: 可选,若指定仅返回该子类型(例如 "column"、"index") func (q *MySQLDriver) GetChildObjects(ctx context.Context, path meta.ObjectPath, childType string, page, pageSize int, fetch bool) ([]meta.GenericObject, int64, error) { db := q.db // path 必须至少包含父对象自身 if len(path) == 0 { return nil, 0, fmt.Errorf("empty path") } parentEntry := path[len(path)-1] // parentEntry.Name 在库级场景下为库名(TABLE_SCHEMA) dbName := parentEntry.Name // 解析筛选类型(默认 table),优先使用 childType 参数 objectType := strings.ToLower(childType) if objectType == "" { objectType = "table" } // 当不需要拉取具体数据时,返回可用类型清单(前端据此发起具体类型的请求) if !fetch { // 目前不再支持传入表级 parent 来列出表级子类型(请使用 GetObjectDetails 获取表的列/索引等信息) // 为每种类型占位添加 Attrs;Description 已移到结构体字段 types := []meta.GenericObject{ {ID: fmt.Sprintf("%s.type-table", dbName), Name: "table", Description: "库级的表类型占位;前端可据此请求 /database/metadata/children 获取具体表列表", Type: "table", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{}}, {ID: fmt.Sprintf("%s.type-view", dbName), Name: "view", Description: "视图(view)类型占位;前端可据此请求 /database/metadata/children?type=view", Type: "view", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{}}, {ID: fmt.Sprintf("%s.type-index", dbName), Name: "index", Description: "索引(index)类型占位;可用于列出库级索引或提示按表查看索引", Type: "index", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{}}, {ID: fmt.Sprintf("%s.type-procedure", dbName), Name: "procedure", Description: "存储过程(procedure)类型占位;前端可据此请求对应类型列表", Type: "procedure", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{}}, {ID: fmt.Sprintf("%s.type-trigger", dbName), Name: "trigger", Description: "触发器(trigger)类型占位;前端可据此请求对应触发器列表", Type: "trigger", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{}}, } return types, 0, nil } switch objectType { case "table": tableType := "BASE TABLE" objType := "table" // count var total int64 if err := db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = ? `, dbName, tableType).Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询库 %s 中 %s 数量失败:%w", dbName, tableType, err) } // page offset := (page - 1) * pageSize rows, err := db.QueryContext(ctx, ` SELECT TABLE_NAME, ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = ? LIMIT ? OFFSET ? `, dbName, tableType, pageSize, offset) if err != nil { return nil, 0, fmt.Errorf("分页查询库 %s 的 %s 列表失败(page=%d,pageSize=%d):%w", dbName, tableType, page, pageSize, err) } defer rows.Close() var objects []meta.GenericObject for rows.Next() { var tName sql.NullString var engine sql.NullString var createTime sql.NullString if err := rows.Scan(&tName, &engine, &createTime); err != nil { continue } objects = append(objects, meta.GenericObject{ ID: fmt.Sprintf("%s.table-%s", dbName, tName.String), Name: tName.String, Type: objType, ParentID: dbName, DBType: "mysql", Attrs: map[string]string{ "engine": engine.String, "createTime": createTime.String, }, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历表结果集出错(%s.%s):%w", dbName, tableType, err) } return objects, total, nil case "view": tableType := "VIEW" objType := "view" // count var total int64 if err := db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = ? `, dbName, tableType).Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询库 %s 中 %s 数量失败:%w", dbName, tableType, err) } // page offset := (page - 1) * pageSize rows, err := db.QueryContext(ctx, ` SELECT TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = ? LIMIT ? OFFSET ? `, dbName, tableType, pageSize, offset) if err != nil { return nil, 0, fmt.Errorf("分页查询库 %s 的 %s 列表失败(page=%d,pageSize=%d):%w", dbName, tableType, page, pageSize, err) } defer rows.Close() var objects []meta.GenericObject for rows.Next() { var tName sql.NullString var engine sql.NullString var createTime sql.NullString if err := rows.Scan(&tName, &engine, &createTime); err != nil { continue } objects = append(objects, meta.GenericObject{ ID: fmt.Sprintf("%s.table-%s", dbName, tName.String), Name: tName.String, Type: objType, ParentID: dbName, DBType: "mysql", Attrs: map[string]string{ "engine": engine.String, "createTime": createTime.String, }, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历视图结果集出错(%s.%s):%w", dbName, tableType, err) } return objects, total, nil case "index": // 仅支持库级索引列出(表级索引请通过 GetObjectDetails 读取表详情以获取索引) var total int64 if err := db.QueryRowContext(ctx, ` SELECT COUNT(DISTINCT CONCAT(TABLE_NAME,'::',INDEX_NAME)) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? `, dbName).Scan(&total); err != nil { return nil, 0, err } offset := (page - 1) * pageSize rows, err := db.QueryContext(ctx, ` SELECT DISTINCT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? LIMIT ? OFFSET ? `, dbName, pageSize, offset) if err != nil { return nil, 0, err } defer rows.Close() var objects []meta.GenericObject for rows.Next() { var idxName sql.NullString var tName sql.NullString var nonUnique sql.NullInt64 if err := rows.Scan(&idxName, &tName, &nonUnique); err != nil { continue } id := fmt.Sprintf("%s.index-%s-%s", dbName, tName.String, idxName.String) objects = append(objects, meta.GenericObject{ ID: id, Name: idxName.String, Type: "index", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{ "table": tName.String, "nonUnique": fmt.Sprintf("%d", nonUnique.Int64), }, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历索引结果集出错(库级,%s):%w", dbName, err) } return objects, total, nil case "procedure", "proc": var total int64 if err := db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' `, dbName).Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询库 %s 存储过程数量失败:%w", dbName, err) } offset := (page - 1) * pageSize rows, err := db.QueryContext(ctx, ` SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' LIMIT ? OFFSET ? `, dbName, pageSize, offset) if err != nil { return nil, 0, fmt.Errorf("分页查询库 %s 存储过程列表失败(page=%d,pageSize=%d):%w", dbName, page, pageSize, err) } defer rows.Close() var objects []meta.GenericObject for rows.Next() { var rName sql.NullString var def sql.NullString var created sql.NullString if err := rows.Scan(&rName, &def, &created); err != nil { continue } id := fmt.Sprintf("%s.proc-%s", dbName, rName.String) objects = append(objects, meta.GenericObject{ ID: id, Name: rName.String, Type: "procedure", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{ "definition": def.String, "created": created.String, }, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历存储过程结果出错(%s):%w", dbName, err) } return objects, total, nil case "trigger": // 列出库中的触发器(不再支持表级 parentName) var total int64 if err := db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? `, dbName).Scan(&total); err != nil { return nil, 0, fmt.Errorf("查询库 %s 触发器数量失败:%w", dbName, err) } offset := (page - 1) * pageSize rows, err := db.QueryContext(ctx, ` SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? LIMIT ? OFFSET ? `, dbName, pageSize, offset) if err != nil { return nil, 0, fmt.Errorf("分页查询库 %s 触发器列表失败(page=%d,pageSize=%d):%w", dbName, page, pageSize, err) } defer rows.Close() var objects []meta.GenericObject for rows.Next() { var tName sql.NullString var event sql.NullString var objTable sql.NullString var timing sql.NullString var stmt sql.NullString if err := rows.Scan(&tName, &event, &objTable, &timing, &stmt); err != nil { continue } id := fmt.Sprintf("%s.trigger-%s", dbName, tName.String) objects = append(objects, meta.GenericObject{ ID: id, Name: tName.String, Type: "trigger", ParentID: dbName, DBType: "mysql", Attrs: map[string]string{ "event": event.String, "table": objTable.String, "timing": timing.String, "statement": stmt.String, }, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历触发器结果出错(%s):%w", dbName, err) } return objects, total, nil default: return nil, 0, fmt.Errorf("不支持的类型: %s", objectType) } } // GetObjectDetails 获取对象详情(含子对象,如表的字段、索引) // path: 从根到目标对象的完整路径(见 meta.ObjectPath),最后一项为目标对象 // 注意:不再兼容旧的 dotted objectID 格式,调用方应传入结构化路径 func (q *MySQLDriver) GetObjectDetails(ctx context.Context, path meta.ObjectPath, fetch bool) (meta.GenericObject, error) { if len(path) == 0 { return meta.GenericObject{}, fmt.Errorf("empty path") } last := path[len(path)-1] t := strings.ToLower(last.Type) switch t { case "table", "view": tableName := last.Name var dbName string if len(path) >= 2 { dbName = path[len(path)-2].Name } else { dbName = "" } // 构造表对象的 ID 与 parentID parentID := dbName objectID := fmt.Sprintf("%s.table-%s", parentID, tableName) // 获取表基本信息 tableObj, err := q.getTableBaseInfo(ctx, dbName, tableName, parentID) if err != nil { return meta.GenericObject{}, fmt.Errorf("获取表 %s.%s 的基础信息失败:%w", dbName, tableName, err) } // 如果不需要拉取子对象细节(如列/索引),返回可用子类型清单(供前端按类型按需加载) if !fetch { var types []meta.GenericObject // columns types = append(types, meta.GenericObject{ ID: fmt.Sprintf("%s.type-column", objectID), Name: "column", Description: "列(column)类型占位;调用同一对象详情接口并设置 fetch=true 可获取列列表", Type: "column", ParentID: objectID, DBType: "mysql", Attrs: map[string]string{}, }) // indexes types = append(types, meta.GenericObject{ ID: fmt.Sprintf("%s.type-index", objectID), Name: "index", Description: "索引(index)类型占位;调用对象详情接口并设置 fetch=true 可获取索引信息", Type: "index", ParentID: objectID, DBType: "mysql", Attrs: map[string]string{}, }) // 将 types 作为 Children 返回(空的 Children 表示未展开,带 types 表示可按类型加载) tableObj.Children = types return tableObj, nil } // 查询字段(子对象) columns, err := q.getTableColumns(ctx, dbName, tableName, objectID) if err != nil { return meta.GenericObject{}, fmt.Errorf("获取表 %s.%s 列信息失败:%w", dbName, tableName, err) } tableObj.Children = append(tableObj.Children, columns...) // 查询索引(子对象) indexes, err := q.getTableIndexes(ctx, dbName, tableName, objectID) if err != nil { return meta.GenericObject{}, fmt.Errorf("获取表 %s.%s 索引信息失败:%w", dbName, tableName, err) } tableObj.Children = append(tableObj.Children, indexes...) return tableObj, nil default: return meta.GenericObject{}, fmt.Errorf("不支持的对象类型: %s", last.Type) } } // getTableBaseInfo 返回表的基础信息,用于构造 table 对象 func (q *MySQLDriver) getTableBaseInfo(ctx context.Context, dbName, tableName, parentName string) (meta.GenericObject, error) { db := q.db var engine sql.NullString var tableType sql.NullString var createTime sql.NullString if err := db.QueryRowContext(ctx, ` SELECT ENGINE, TABLE_TYPE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? `, dbName, tableName).Scan(&engine, &tableType, &createTime); err != nil { return meta.GenericObject{}, fmt.Errorf("查询表 %s.%s 的基础信息失败:%w", dbName, tableName, err) } objType := "table" if strings.EqualFold(tableType.String, "VIEW") { objType = "view" } parentID := parentName objectID := fmt.Sprintf("%s.table-%s", parentName, tableName) return meta.GenericObject{ ID: objectID, Name: tableName, Type: objType, ParentID: parentID, DBType: "mysql", Attrs: map[string]string{ "engine": engine.String, "createTime": createTime.String, }, Children: []meta.GenericObject{}, }, nil } // getTableColumns 返回表的列信息(作为子对象) func (q *MySQLDriver) getTableColumns(ctx context.Context, dbName, tableName, objectID string) ([]meta.GenericObject, error) { db := q.db rows, err := db.QueryContext(ctx, ` SELECT COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, CHARACTER_MAXIMUM_LENGTH, ORDINAL_POSITION FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION `, dbName, tableName) if err != nil { return nil, fmt.Errorf("查询表 %s.%s 的列信息失败:%w", dbName, tableName, err) } defer rows.Close() var cols []meta.GenericObject for rows.Next() { var colName sql.NullString var columnType sql.NullString var dataType sql.NullString var isNullable sql.NullString var colDefault sql.NullString var charMax sql.NullInt64 var ord sql.NullInt64 if err := rows.Scan(&colName, &columnType, &dataType, &isNullable, &colDefault, &charMax, &ord); err != nil { continue } id := fmt.Sprintf("%s.column-%s", objectID, colName.String) cols = append(cols, meta.GenericObject{ ID: id, Name: colName.String, Type: "column", ParentID: objectID, DBType: "mysql", Attrs: map[string]string{ "columnType": columnType.String, "dataType": dataType.String, "nullable": isNullable.String, "default": colDefault.String, "charMax": fmt.Sprintf("%d", charMax.Int64), "position": fmt.Sprintf("%d", ord.Int64), }, }) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("遍历列结果集出错(%s.%s):%w", dbName, tableName, err) } return cols, nil } // getTableIndexes 返回表的索引信息(作为子对象) func (q *MySQLDriver) getTableIndexes(ctx context.Context, dbName, tableName, objectID string) ([]meta.GenericObject, error) { db := q.db rows, err := db.QueryContext(ctx, ` SELECT INDEX_NAME, NON_UNIQUE, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? GROUP BY INDEX_NAME, NON_UNIQUE `, dbName, tableName) if err != nil { return nil, fmt.Errorf("查询表 %s.%s 的索引信息失败:%w", dbName, tableName, err) } defer rows.Close() var idxs []meta.GenericObject for rows.Next() { var idxName sql.NullString var nonUnique sql.NullInt64 var cols sql.NullString if err := rows.Scan(&idxName, &nonUnique, &cols); err != nil { continue } id := fmt.Sprintf("%s.index-%s", objectID, idxName.String) idxs = append(idxs, meta.GenericObject{ ID: id, Name: idxName.String, Type: "index", ParentID: objectID, DBType: "mysql", Attrs: map[string]string{ "columns": cols.String, "nonUnique": fmt.Sprintf("%d", nonUnique.Int64), }, }) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("遍历索引结果集出错(%s.%s):%w", dbName, tableName, err) } return idxs, nil } // DeleteRootObjects:根据前端传入的 rootName(支持通配符 * 或 SQL %)、typeName(如 database/table/view) // 返回匹配到的对象列表与总数(注:默认不直接执行 DROP,仅列出匹配项) func (q *MySQLDriver) DeleteRootObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error) { var resp meta.ObjectOperationResponse db := q.db rootName := req.Object.Name typeName := req.Object.Type t := strings.ToLower(typeName) // 处理通配符 pattern := rootName if strings.Contains(pattern, "*") { pattern = strings.ReplaceAll(pattern, "*", "%") } useLike := strings.Contains(pattern, "%") || strings.Contains(pattern, "_") switch t { case "database", "schema": var rows *sql.Rows var err error if pattern == "" { rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA`) } else if useLike { rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE ?`, pattern) } else { rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, pattern) } if err != nil { return resp, fmt.Errorf("查询 schema 列表以供删除匹配失败:%w", err) } defer rows.Close() var objs []meta.GenericObject var total int64 for rows.Next() { var name sql.NullString var charset sql.NullString if err := rows.Scan(&name, &charset); err != nil { continue } total++ objs = append(objs, meta.GenericObject{ ID: fmt.Sprintf("db-%s", name.String), Name: name.String, Type: "database", ParentID: "", DBType: "mysql", Attrs: map[string]string{ "charset": charset.String, }, }) } if err := rows.Err(); err != nil { return resp, fmt.Errorf("遍历 schema 结果出错:%w", err) } resp.Affected = total resp.ObjectID = "" // 将匹配对象放入 Options 供前端显示(不能放在 Object 因为可能是多个) respMap := map[string]interface{}{"matches": objs} if respMapBytes, err := json.Marshal(respMap); err == nil { // 当仅生成 SQL(不执行)时,把匹配对象的 JSON 放到 Sql 字段供前端查看; // 若 Execute==true,则我们会尝试执行对应的 DROP 语句并在 Sql 中也返回执行的语句列表。 resp.Sql = string(respMapBytes) } // 如果请求不要求执行,直接返回列出匹配项 if !req.Execute { return resp, nil } // Execute==true: 执行删除操作(注意:对 DDL 的事务语义依赖于具体数据库;MySQL 的 DROP 在多数情况下不可回滚) tx, err := db.BeginTx(ctx, nil) if err != nil { return resp, fmt.Errorf("开始事务失败:%w", err) } var execCount int64 var execSQLs []string for _, o := range objs { // 对不同类型生成对应的 DROP 语句 switch o.Type { case "database": sqlStr := fmt.Sprintf("DROP DATABASE `%s`", o.Name) if _, err := tx.ExecContext(ctx, sqlStr); err != nil { _ = tx.Rollback() return resp, fmt.Errorf("执行 SQL 失败:%s, err: %w", sqlStr, err) } execSQLs = append(execSQLs, sqlStr) execCount++ default: // 其他 root 类型目前不支持直接删除,记录并继续 } } if err := tx.Commit(); err != nil { _ = tx.Rollback() return resp, fmt.Errorf("提交事务失败:%w", err) } resp.Affected = execCount if b, err := json.Marshal(execSQLs); err == nil { resp.Sql = string(b) } return resp, nil case "table", "view": tableType := "BASE TABLE" if t == "view" { tableType = "VIEW" } var rows *sql.Rows var err error if pattern == "" { rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ?`, tableType) } else if useLike { rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME LIKE ?`, tableType, pattern) } else { rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME = ?`, tableType, pattern) } if err != nil { return resp, fmt.Errorf("查询表/视图以供删除匹配失败:%w", err) } defer rows.Close() var objs []meta.GenericObject var total int64 for rows.Next() { var schema sql.NullString var tname sql.NullString var engine sql.NullString var ctime sql.NullString if err := rows.Scan(&schema, &tname, &engine, &ctime); err != nil { continue } total++ pid := fmt.Sprintf("db-%s", schema.String) id := fmt.Sprintf("%s.table-%s", pid, tname.String) objs = append(objs, meta.GenericObject{ ID: id, Name: tname.String, Type: t, ParentID: pid, DBType: "mysql", Attrs: map[string]string{ "engine": engine.String, "createTime": ctime.String, }, }) } if err := rows.Err(); err != nil { return resp, fmt.Errorf("遍历表/视图结果出错:%w", err) } resp.Affected = total matchMap := map[string]interface{}{"matches": objs} if b, err := json.Marshal(matchMap); err == nil { resp.Sql = string(b) } return resp, nil default: return resp, fmt.Errorf("不支持的 root 类型: %s", typeName) } } // DeleteChildObjects:根据 parentID(如 db-xxx 或 conn.db-xxx)和 filter(type/name)返回匹配的子对象列表与总数 func (q *MySQLDriver) DeleteChildObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error) { var resp meta.ObjectOperationResponse parentID := req.Object.ParentID filter := map[string]string{} if req.Object.Name != "" { filter["name"] = req.Object.Name } if req.Object.Type != "" { filter["type"] = req.Object.Type } // reuse previous logic but adapt to return ObjectOperationResponse parts := strings.Split(parentID, ".") var dbPart string if len(parts) >= 2 { dbPart = parts[1] } else { dbPart = parentID } dbName := strings.TrimPrefix(dbPart, "db-") objectType := strings.ToLower(filter["type"]) namePattern := filter["name"] if strings.Contains(namePattern, "*") { namePattern = strings.ReplaceAll(namePattern, "*", "%") } useLike := strings.Contains(namePattern, "%") || strings.Contains(namePattern, "_") db := q.db switch objectType { case "index": // 表级或库级索引 var rows *sql.Rows var err error if namePattern == "" { rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName) } else if useLike { rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME LIKE ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern) } else { rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern) } if err != nil { return resp, fmt.Errorf("查询索引以供删除匹配失败:%w", err) } defer rows.Close() var res []meta.GenericObject var total int64 for rows.Next() { var idx sql.NullString var tname sql.NullString var nonUnique sql.NullInt64 if err := rows.Scan(&idx, &tname, &nonUnique); err != nil { continue } total++ id := fmt.Sprintf("%s.table-%s.index-%s", parentID, tname.String, idx.String) res = append(res, meta.GenericObject{ ID: id, Name: idx.String, Type: "index", ParentID: fmt.Sprintf("%s.table-%s", parentID, tname.String), DBType: "mysql", Attrs: map[string]string{ "table": tname.String, "nonUnique": fmt.Sprintf("%d", nonUnique.Int64), }, }) } if err := rows.Err(); err != nil { return resp, fmt.Errorf("遍历索引结果出错:%w", err) } resp.Affected = total m := map[string]interface{}{"matches": res} if b, err := json.Marshal(m); err == nil { resp.Sql = string(b) } return resp, nil case "procedure", "proc": var rows *sql.Rows var err error if namePattern == "" { rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE'`, dbName) } else if useLike { rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME LIKE ?`, dbName, namePattern) } else { rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ?`, dbName, namePattern) } if err != nil { return resp, fmt.Errorf("查询存储过程以供删除匹配失败:%w", err) } defer rows.Close() var res []meta.GenericObject var total int64 for rows.Next() { var rName sql.NullString var def sql.NullString var created sql.NullString if err := rows.Scan(&rName, &def, &created); err != nil { continue } total++ id := fmt.Sprintf("%s.proc-%s", parentID, rName.String) res = append(res, meta.GenericObject{ ID: id, Name: rName.String, Type: "procedure", ParentID: parentID, DBType: "mysql", Attrs: map[string]string{ "definition": def.String, "created": created.String, }, }) } if err := rows.Err(); err != nil { return resp, fmt.Errorf("遍历存储过程结果出错:%w", err) } resp.Affected = total m := map[string]interface{}{"matches": res} if b, err := json.Marshal(m); err == nil { resp.Sql = string(b) } return resp, nil case "trigger": var rows *sql.Rows var err error if namePattern == "" { rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ?`, dbName) } else if useLike { rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME LIKE ?`, dbName, namePattern) } else { rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME = ?`, dbName, namePattern) } if err != nil { return resp, fmt.Errorf("查询触发器以供删除匹配失败:%w", err) } defer rows.Close() var res []meta.GenericObject var total int64 for rows.Next() { var tName sql.NullString var event sql.NullString var objTable sql.NullString var timing sql.NullString var stmt sql.NullString if err := rows.Scan(&tName, &event, &objTable, &timing, &stmt); err != nil { continue } total++ id := fmt.Sprintf("%s.trigger-%s", parentID, tName.String) res = append(res, meta.GenericObject{ ID: id, Name: tName.String, Type: "trigger", ParentID: parentID, DBType: "mysql", Attrs: map[string]string{ "event": event.String, "table": objTable.String, "timing": timing.String, "statement": stmt.String, }, }) } if err := rows.Err(); err != nil { return resp, fmt.Errorf("遍历触发器结果出错:%w", err) } resp.Affected = total m := map[string]interface{}{"matches": res} if b, err := json.Marshal(m); err == nil { resp.Sql = string(b) } return resp, nil default: return resp, fmt.Errorf("不支持的 child 类型: %s", objectType) } } // DescribeCreateTemplate 返回创建指定类型对象的表单模板(供前端渲染) // getDatabaseTemplateFields 返回数据库相关的模板字段 func getDatabaseTemplateFields(isUpdate bool, currentValues map[string]string) []meta.TemplateField { fields := []meta.TemplateField{ {Name: "databaseName", Label: "Database Name", Type: meta.FieldTypeString, Required: true}, {Name: "charset", Label: "Character Set", Type: meta.FieldTypeEnum, Required: false, Default: "utf8mb4", EnumOptions: []string{"utf8mb4", "utf8", "latin1", "ascii", "ucs2", "utf16", "utf32", "big5", "gbk"}}, {Name: "collation", Label: "Collation", Type: meta.FieldTypeString, Required: false}, } if !isUpdate { // 创建模式:添加ifNotExists字段 fields = append(fields, meta.TemplateField{Name: "ifNotExists", Label: "If Not Exists", Type: meta.FieldTypeBool, Required: false, Default: "false"}) fields[0].Help = "要创建的数据库名称,不含前缀" } else { // 修改模式:设置当前值和可编辑性(currentValues 采用 map[string]string) if currentValues != nil { if dbName, ok := currentValues["databaseName"]; ok { fields[0].Current = dbName fields[0].Editable = &[]bool{false}[0] // 数据库名通常不可修改 } if charset, ok := currentValues["charset"]; ok { fields[1].Current = charset } if collation, ok := currentValues["collation"]; ok { fields[2].Current = collation } } } return fields } // getTableTemplateFields 返回表相关的模板字段 func getTableTemplateFields(isUpdate bool, currentValues map[string]string) []meta.TemplateField { fields := []meta.TemplateField{ {Name: "tableName", Label: "Table Name", Type: meta.FieldTypeString, Required: true}, {Name: "engine", Label: "Engine", Type: meta.FieldTypeEnum, Required: false, EnumOptions: []string{"InnoDB", "MyISAM"}, Default: "InnoDB"}, {Name: "charset", Label: "Charset", Type: meta.FieldTypeEnum, Required: false, Default: "utf8mb4", EnumOptions: []string{"utf8mb4", "utf8", "latin1", "ascii", "ucs2", "utf16", "utf32", "big5", "gbk"}}, {Name: "collation", Label: "Collation", Type: meta.FieldTypeString, Required: false}, } if !isUpdate { // 创建模式:添加ifNotExists和columns字段 fields = append(fields, meta.TemplateField{Name: "ifNotExists", Label: "If Not Exists", Type: meta.FieldTypeBool, Required: false, Default: "false"}) fields = append(fields, meta.TemplateField{ Name: "columns", Label: "Columns", Type: meta.FieldTypeList, Required: true, NestedFields: []meta.TemplateField{ {Name: "name", Label: "Column Name", Type: meta.FieldTypeString, Required: true}, {Name: "type", Label: "Data Type", Type: meta.FieldTypeString, Required: true}, {Name: "nullable", Label: "Nullable", Type: meta.FieldTypeBool, Required: false, Default: "false"}, {Name: "default", Label: "Default", Type: meta.FieldTypeString, Required: false}, {Name: "autoIncrement", Label: "Auto Increment", Type: meta.FieldTypeBool, Required: false, Default: "false"}, }}) } else { // 修改模式:设置当前值和可编辑性(currentValues 采用 map[string]string) if currentValues != nil { if tableName, ok := currentValues["tableName"]; ok { fields[0].Current = tableName fields[0].Editable = &[]bool{false}[0] // 表名修改需要重命名 } if engine, ok := currentValues["engine"]; ok { fields[1].Current = engine } if charset, ok := currentValues["charset"]; ok { fields[2].Current = charset } if collation, ok := currentValues["collation"]; ok { fields[3].Current = collation } } } return fields } // getIndexTemplateFields 返回索引相关的模板字段 func getIndexTemplateFields(isUpdate bool, currentValues map[string]string) []meta.TemplateField { fields := []meta.TemplateField{ {Name: "indexName", Label: "Index Name", Type: meta.FieldTypeString, Required: true}, {Name: "columns", Label: "Columns (comma separated)", Type: meta.FieldTypeString, Required: true}, {Name: "unique", Label: "Unique", Type: meta.FieldTypeBool, Required: false, Default: "false"}, } if isUpdate && currentValues != nil { // 修改模式:设置当前值(currentValues 为 map[string]string) if indexName, ok := currentValues["indexName"]; ok { fields[0].Current = indexName } if columns, ok := currentValues["columns"]; ok { fields[1].Current = columns } if uniqueStr, ok := currentValues["unique"]; ok { fields[2].Current = uniqueStr } } return fields } // getCurrentDatabaseInfo 获取数据库的当前信息 func (q *MySQLDriver) getCurrentDatabaseInfo(ctx context.Context, dbName string) (map[string]string, error) { var charset, collation sql.NullString query := "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?" err := q.db.QueryRowContext(ctx, query, dbName).Scan(&charset, &collation) if err != nil { return nil, fmt.Errorf("获取数据库信息失败: %w", err) } return map[string]string{ "databaseName": dbName, "charset": charset.String, "collation": collation.String, }, nil } // getCurrentTableInfo 获取表的当前信息 func (q *MySQLDriver) getCurrentTableInfo(ctx context.Context, dbName, tableName string) (map[string]string, error) { var engine, charset, collation sql.NullString query := "SELECT ENGINE, TABLE_COLLATION, SUBSTRING_INDEX(TABLE_COLLATION, '_', 1) AS TABLE_CHARSET FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?" err := q.db.QueryRowContext(ctx, query, dbName, tableName).Scan(&engine, &collation, &charset) if err != nil { return nil, fmt.Errorf("获取表信息失败: %w", err) } return map[string]string{ "tableName": tableName, "engine": engine.String, "charset": charset.String, "collation": collation.String, }, nil } func (q *MySQLDriver) DescribeCreateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) { // 从路径中提取对象类型和父名称 var objectType, parentName string if len(path) > 0 { lastEntry := path[len(path)-1] objectType = strings.ToLower(lastEntry.Type) // 对于创建操作,最后一个元素是待创建的对象类型 // 前面的元素构成父上下文 if len(path) > 1 { parentEntry := path[len(path)-2] // 父对象是倒数第二个元素 parentName = parentEntry.Name } } else { return meta.ObjectTemplate{}, fmt.Errorf("创建模板需要指定对象类型路径") } switch objectType { case "database": // 示例值均以字符串形式表示 ex := map[string]string{"databaseName": "mydb", "charset": "utf8mb4", "ifNotExists": "true"} tpl := meta.ObjectTemplate{ Operation: "create", ObjectType: "database", ParentHint: "", Fields: getDatabaseTemplateFields(false, nil), Example: ex, } return tpl, nil case "table": parentHint := "parentName should be the database name" if parentName != "" { parentHint = "database: " + parentName } // 将复杂的 columns 示例编码为 JSON 字符串以便前端展示/解析 colsExample := []map[string]interface{}{{"name": "id", "type": "INT", "nullable": false}} colsB, _ := json.Marshal(colsExample) ex := map[string]string{"tableName": "users", "columns": string(colsB), "engine": "InnoDB"} tpl := meta.ObjectTemplate{ Operation: "create", ObjectType: "table", ParentHint: parentHint, Fields: getTableTemplateFields(false, nil), Example: ex, } return tpl, nil case "index": parentHint := "parentName can be table name" if parentName != "" { parentHint = "table: " + parentName } ex := map[string]string{"indexName": "idx_users_email", "columns": "email", "unique": "true"} tpl := meta.ObjectTemplate{ Operation: "create", ObjectType: "index", ParentHint: parentHint, Fields: getIndexTemplateFields(false, nil), Example: ex, } return tpl, nil default: return meta.ObjectTemplate{}, fmt.Errorf("不支持的 create 类型: %s", objectType) } } // GetMetadataInfo 返回数据库的元信息(关键字、字段类型、能力等) func (q *MySQLDriver) GetMetadataInfo(ctx context.Context) (meta.MetadataCapabilities, error) { var caps meta.MetadataCapabilities // 1) 尝试从 INFORMATION_SCHEMA.COLUMNS 获取字段类型 rows, err := q.db.QueryContext(ctx, "SELECT DISTINCT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS") if err == nil { defer rows.Close() for rows.Next() { var dt sql.NullString if err := rows.Scan(&dt); err != nil { continue } if dt.Valid && dt.String != "" { caps.FieldTypes = append(caps.FieldTypes, strings.ToUpper(dt.String)) } } } else { // 回退到一组常见类型 caps.FieldTypes = []string{"INT", "BIGINT", "VARCHAR", "TEXT", "DATETIME", "TIMESTAMP", "DATE", "CHAR", "FLOAT", "DOUBLE", "DECIMAL", "BOOLEAN"} } // 2) 尝试从 mysql.help_keyword 获取关键字(部分 MySQL 安装提供) rows2, err2 := q.db.QueryContext(ctx, "SELECT DISTINCT word FROM mysql.help_keyword") if err2 == nil { defer rows2.Close() for rows2.Next() { var kw sql.NullString if err := rows2.Scan(&kw); err != nil { continue } if kw.Valid && kw.String != "" { caps.Keywords = append(caps.Keywords, kw.String) } } } else { // 3) 尝试 INFORMATION_SCHEMA.KEYWORDS(部分版本可用) rows3, err3 := q.db.QueryContext(ctx, "SELECT DISTINCT keyword FROM INFORMATION_SCHEMA.KEYWORDS") if err3 == nil { defer rows3.Close() for rows3.Next() { var kw sql.NullString if err := rows3.Scan(&kw); err != nil { continue } if kw.Valid && kw.String != "" { caps.Keywords = append(caps.Keywords, kw.String) } } } } // 4) 去重与排序 if len(caps.FieldTypes) > 0 { m := map[string]struct{}{} for _, v := range caps.FieldTypes { m[v] = struct{}{} } caps.FieldTypes = caps.FieldTypes[:0] for k := range m { caps.FieldTypes = append(caps.FieldTypes, k) } sort.Strings(caps.FieldTypes) } if len(caps.Keywords) > 0 { m := map[string]struct{}{} for _, v := range caps.Keywords { m[v] = struct{}{} } caps.Keywords = caps.Keywords[:0] for k := range m { caps.Keywords = append(caps.Keywords, k) } sort.Strings(caps.Keywords) } // 5) 常见能力标记 caps.Capabilities = map[string]bool{ "supportsTransactions": true, "supportsDDLTransaction": false, } return caps, nil } // CreateObject 仅实现 preview(execute==false)路径:校验输入并生成 SQL,execute=true 尚未实现 func (q *MySQLDriver) CreateObject(ctx context.Context, req meta.CreateObjectRequest) (meta.CreateObjectResponse, error) { var resp meta.CreateObjectResponse t := strings.ToLower(req.ObjectType) // Basic validation if req.ObjectType == "" { return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: objectType") } switch t { case "database": props := req.Properties nameI, ok := props["databaseName"] if !ok { return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: databaseName") } name, _ := nameI.(string) if name == "" { return meta.CreateObjectResponse{}, fmt.Errorf("databaseName 不能为空") } if req.Execute { return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)") } charset := "utf8mb4" if cs, ok := props["charset"].(string); ok && cs != "" { charset = cs } ifNot := false if v, ok := props["ifNotExists"].(bool); ok { ifNot = v } sql := "CREATE DATABASE" if ifNot { sql += " IF NOT EXISTS" } sql = fmt.Sprintf("%s `%s` DEFAULT CHARACTER SET = %s;", sql, name, charset) resp.GeneratedSQL = []string{sql} return resp, nil case "table": props := req.Properties tnameI, ok := props["tableName"] if !ok { return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: tableName") } tname, _ := tnameI.(string) if tname == "" { return meta.CreateObjectResponse{}, fmt.Errorf("tableName 不能为空") } colsI, ok := props["columns"] if !ok { return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: columns") } colsSlice, ok := colsI.([]interface{}) if !ok { return meta.CreateObjectResponse{}, fmt.Errorf("columns 格式无效,期望为数组类型的列定义,例如 [{\"name\":\"id\",\"type\":\"INT\"}]") } if req.Execute { return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)") } engine := "InnoDB" if e, ok := props["engine"].(string); ok && e != "" { engine = e } charset := "utf8mb4" if cs, ok := props["charset"].(string); ok && cs != "" { charset = cs } ifNot := false if v, ok := props["ifNotExists"].(bool); ok { ifNot = v } // build column definitions var colDefs []string for _, ci := range colsSlice { m, ok := ci.(map[string]interface{}) if !ok { continue } cname, _ := m["name"].(string) ctype, _ := m["type"].(string) if cname == "" || ctype == "" { // skip invalid continue } nullable := true if n, ok := m["nullable"].(bool); ok { nullable = n } autoInc := false if a, ok := m["autoIncrement"].(bool); ok { autoInc = a } defStr := "" if d, ok := m["default"]; ok && d != nil { // simple formatting: quote strings, otherwise use fmt.Sprint switch v := d.(type) { case string: defStr = fmt.Sprintf(" DEFAULT '%s'", strings.ReplaceAll(v, "'", "\\'")) default: defStr = fmt.Sprintf(" DEFAULT %v", v) } } col := fmt.Sprintf("`%s` %s", cname, ctype) if !nullable { col += " NOT NULL" } if autoInc { col += " AUTO_INCREMENT" } col += defStr colDefs = append(colDefs, col) } if len(colDefs) == 0 { return meta.CreateObjectResponse{}, fmt.Errorf("未找到有效的列定义,请检查 properties.columns 字段,示例格式: [{\"name\":\"id\",\"type\":\"INT\"}]") } // build create statement (include parent/db if provided) createStmt := "CREATE TABLE" if ifNot { createStmt += " IF NOT EXISTS" } // If parent provided, include database qualifier if req.ParentName != "" { createStmt = fmt.Sprintf("%s `%s`.`%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, req.ParentName, tname, strings.Join(colDefs, ", "), engine, charset) } else { createStmt = fmt.Sprintf("%s `%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, tname, strings.Join(colDefs, ", "), engine, charset) } resp.GeneratedSQL = []string{createStmt} return resp, nil case "index": props := req.Properties iname, _ := props["indexName"].(string) cols, _ := props["columns"].(string) unique := false if u, ok := props["unique"].(bool); ok { unique = u } if iname == "" || cols == "" { return meta.CreateObjectResponse{}, fmt.Errorf("indexName 与 columns 为必填字段") } if req.Execute { return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)") } uq := "" if unique { uq = "UNIQUE " } // parentName ideally should be table name tableRef := iname if req.ParentName != "" { tableRef = req.ParentName } stmt := fmt.Sprintf("CREATE %sINDEX `%s` ON `%s` (%s);", uq, iname, tableRef, cols) resp.GeneratedSQL = []string{stmt} return resp, nil default: return meta.CreateObjectResponse{}, fmt.Errorf("不支持的 create 类型: %s", req.ObjectType) } } // QueryData 执行数据查询(实现 DataReader 接口) func (q *MySQLDriver) QueryData(ctx context.Context, path meta.ObjectPath, req meta.DataQueryRequest) (meta.QueryResult, error) { db := q.db // 解析路径:path 应为 [{"type":"database","name":"db"},{"type":"table","name":"table"}] if len(path) < 2 { return meta.QueryResult{}, fmt.Errorf("invalid path: need at least database and table") } dbName := path[0].Name tableName := path[1].Name // 切换到指定数据库(如果需要) if _, err := db.ExecContext(ctx, fmt.Sprintf("USE `%s`", dbName)); err != nil { return meta.QueryResult{}, fmt.Errorf("failed to switch to database %s: %w", dbName, err) } // 构建列信息 var columns []meta.DataMeta var columnNames []string if len(req.Columns) == 0 { // 查询所有列 query := fmt.Sprintf("SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' ORDER BY ORDINAL_POSITION", dbName, tableName) rows, err := db.QueryContext(ctx, query) if err != nil { return meta.QueryResult{}, fmt.Errorf("failed to query columns: %w", err) } defer rows.Close() for rows.Next() { var colName, dataType, nullable sql.NullString if err := rows.Scan(&colName, &dataType, &nullable); err != nil { continue } columns = append(columns, meta.DataMeta{ Name: colName.String, Type: "column", DBType: dataType.String, Nullable: nullable.String == "YES", }) columnNames = append(columnNames, colName.String) } } else { // 指定列 columnNames = req.Columns for _, col := range req.Columns { columns = append(columns, meta.DataMeta{ Name: col, Type: "column", }) } } // 构建 SELECT 语句 selectClause := "*" if len(columnNames) > 0 { selectClause = "`" + strings.Join(columnNames, "`, `") + "`" } sql := fmt.Sprintf("SELECT %s FROM `%s`", selectClause, tableName) // 添加 WHERE 子句 if len(req.Filters) > 0 { var conditions []string for col, val := range req.Filters { conditions = append(conditions, fmt.Sprintf("`%s` = '%s'", col, val)) // 简单等值,注意 SQL 注入风险(生产环境需参数化) } sql += " WHERE " + strings.Join(conditions, " AND ") } // 添加 ORDER BY if len(req.Order) > 0 { var orders []string for _, o := range req.Order { dir := "ASC" if o.Desc { dir = "DESC" } orders = append(orders, fmt.Sprintf("`%s` %s", o.Column, dir)) } sql += " ORDER BY " + strings.Join(orders, ", ") } // 添加 LIMIT if req.Limit > 0 { sql += fmt.Sprintf(" LIMIT %d", req.Limit) if req.Offset > 0 { sql += fmt.Sprintf(" OFFSET %d", req.Offset) } } // 如果 Fetch=false,仅返回 SQL,不执行查询,但计算总数 if !req.Fetch { var total int64 countSQL := fmt.Sprintf("SELECT COUNT(*) FROM `%s`", tableName) if len(req.Filters) > 0 { var conditions []string for col, val := range req.Filters { conditions = append(conditions, fmt.Sprintf("`%s` = '%s'", col, val)) } countSQL += " WHERE " + strings.Join(conditions, " AND ") } if err := db.QueryRowContext(ctx, countSQL).Scan(&total); err != nil { return meta.QueryResult{}, fmt.Errorf("failed to count total: %w", err) } return meta.QueryResult{ Columns: columns, Rows: []meta.DataMeta{}, Total: &total, Returned: int(total), Truncated: false, Stats: map[string]string{"sql": sql}, }, nil } // 执行查询 rows, err := db.QueryContext(ctx, sql) if err != nil { return meta.QueryResult{}, fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() // 获取列信息(如果未指定列) if len(req.Columns) == 0 { colTypes, err := rows.ColumnTypes() if err != nil { return meta.QueryResult{}, fmt.Errorf("failed to get column types: %w", err) } for i, ct := range colTypes { nullable, _ := ct.Nullable() columns[i].DBType = ct.DatabaseTypeName() columns[i].Nullable = nullable } } // 读取数据 var resultRows []meta.DataMeta for rows.Next() { values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range values { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { return meta.QueryResult{}, fmt.Errorf("failed to scan row: %w", err) } var children []meta.DataMeta for i, val := range values { // 安全检查:确保val是有效的interface{} if i >= len(columns) { continue // 跳过超出列数的字段 } // 处理 []byte 类型,转为 string 以正确显示中文 if val != nil { if b, ok := val.([]byte); ok { val = string(b) } } // 根据值类型设置 ValueType var valueType meta.FieldType if val == nil { valueType = meta.FieldTypeString // nil值当作字符串处理 } else { // 根据值类型设置 ValueType switch reflect.TypeOf(val).Kind() { case reflect.String: valueType = meta.FieldTypeString case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: valueType = meta.FieldTypeInt case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: valueType = meta.FieldTypeInt case reflect.Float32, reflect.Float64: valueType = meta.FieldTypeNumber case reflect.Bool: valueType = meta.FieldTypeBool default: valueType = meta.FieldTypeString // 默认当作字符串 } } children = append(children, meta.DataMeta{ Name: columns[i].Name, Type: "value", Value: val, // 现在 val 是原生类型,但 []byte 已转为 string ValueType: valueType, }) } resultRows = append(resultRows, meta.DataMeta{ Name: getRowName(children), // 设置行 name 为第一个列的值(通常为主键或 ID) Type: "row", RowID: "", Children: children, }) } // 计算总数(如果请求) var total *int64 if req.IncludeTotal { countSQL := fmt.Sprintf("SELECT COUNT(*) FROM `%s`", tableName) if len(req.Filters) > 0 { var conditions []string for col, val := range req.Filters { conditions = append(conditions, fmt.Sprintf("`%s` = '%s'", col, val)) } countSQL += " WHERE " + strings.Join(conditions, " AND ") } var t int64 if err := db.QueryRowContext(ctx, countSQL).Scan(&t); err != nil { return meta.QueryResult{}, fmt.Errorf("failed to count total: %w", err) } total = &t } return meta.QueryResult{ Columns: columns, Rows: resultRows, Total: total, Returned: len(resultRows), Truncated: req.Limit > 0 && len(resultRows) == req.Limit, Stats: map[string]string{"query": sql}, }, nil } // ExecuteSQL 执行SQL语句并返回执行结果 func (q *MySQLDriver) ExecuteSQL(ctx context.Context, path meta.ObjectPath, sql string, params []interface{}) (meta.ExecuteResult, error) { startTime := time.Now() result := meta.ExecuteResult{ Success: false, ExecutionTime: 0, } // 清理和标准化SQL sql = strings.TrimSpace(sql) if sql == "" { result.ErrorMessage = "SQL语句不能为空:请提供有效的SQL查询或执行语句" return result, nil } // 分析SQL类型 sqlType := q.analyzeSQLType(sql) db := q.db // 对于SELECT语句,使用Query执行 if strings.HasPrefix(strings.ToUpper(sqlType), "SELECT") { rows, err := db.QueryContext(ctx, sql, params...) if err != nil { result.ErrorMessage = fmt.Sprintf("SELECT查询执行失败:SQL语法错误或数据库连接问题。原始错误:%v", err) result.ExecutionTime = time.Since(startTime).Milliseconds() return result, nil } defer rows.Close() // 获取列信息 columns, err := rows.Columns() if err != nil { result.ErrorMessage = fmt.Sprintf("获取查询结果列信息失败:无法解析结果集结构。原始错误:%v", err) result.ExecutionTime = time.Since(startTime).Milliseconds() return result, nil } // 构建列元信息 var columnMetas []meta.DataMeta columnTypes, err := rows.ColumnTypes() if err == nil { for i, col := range columns { dbType := "" if i < len(columnTypes) { dbType = columnTypes[i].DatabaseTypeName() } columnMetas = append(columnMetas, meta.DataMeta{ Name: col, Type: "column", DBType: dbType, ValueType: "string", // 简化处理 }) } } // 读取数据 var resultRows []meta.DataMeta rowCount := 0 for rows.Next() && rowCount < 1000 { // 限制最大行数 values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range values { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { result.ErrorMessage = fmt.Sprintf("扫描查询结果行数据失败:数据类型不匹配或结果集损坏。原始错误:%v", err) result.ExecutionTime = time.Since(startTime).Milliseconds() return result, nil } // 构建行数据 var rowValues []meta.DataMeta for i, val := range values { colName := "" if i < len(columns) { colName = columns[i] } // 转换值为字符串 var strVal string if val != nil { strVal = fmt.Sprintf("%v", val) } rowValues = append(rowValues, meta.DataMeta{ Name: colName, Type: "value", Value: strVal, }) } resultRows = append(resultRows, meta.DataMeta{ Type: "row", RowID: "", Children: rowValues, }) rowCount++ } // 构建查询结果 queryResult := &meta.QueryResult{ Columns: columnMetas, Rows: resultRows, Returned: len(resultRows), Truncated: rowCount >= 1000, } result.Success = true result.SQLType = sqlType result.Data = queryResult result.ExecutionTime = time.Since(startTime).Milliseconds() result.RowsReturned = len(resultRows) // 对于非SELECT语句,使用Exec执行 execResult, err := db.ExecContext(ctx, sql, params...) if err != nil { result.ErrorMessage = fmt.Sprintf("%s语句执行失败:SQL语法错误、权限不足或数据库约束冲突。原始错误:%v", sqlType, err) result.ExecutionTime = time.Since(startTime).Milliseconds() return result, nil } // 获取影响行数 affectedRows, _ := execResult.RowsAffected() result.AffectedRows = affectedRows // 获取最后插入ID(如果适用) if strings.HasPrefix(strings.ToUpper(sqlType), "INSERT") { lastInsertID, _ := execResult.LastInsertId() result.LastInsertID = lastInsertID } result.Success = true result.SQLType = sqlType result.ExecutionTime = time.Since(startTime).Milliseconds() } return result, nil } // analyzeSQLType 分析SQL语句类型 func (q *MySQLDriver) analyzeSQLType(sql string) string { upperSQL := strings.ToUpper(strings.TrimSpace(sql)) switch { case strings.HasPrefix(upperSQL, "SELECT"): return "SELECT" case strings.HasPrefix(upperSQL, "INSERT"): return "INSERT" case strings.HasPrefix(upperSQL, "UPDATE"): return "UPDATE" case strings.HasPrefix(upperSQL, "DELETE"): return "DELETE" case strings.HasPrefix(upperSQL, "CREATE"): if strings.Contains(upperSQL, "TABLE") { return "CREATE TABLE" } else if strings.Contains(upperSQL, "DATABASE") { return "CREATE DATABASE" } else if strings.Contains(upperSQL, "INDEX") { return "CREATE INDEX" } return "CREATE" case strings.HasPrefix(upperSQL, "DROP"): if strings.Contains(upperSQL, "TABLE") { return "DROP TABLE" } else if strings.Contains(upperSQL, "DATABASE") { return "DROP DATABASE" } else if strings.Contains(upperSQL, "INDEX") { return "DROP INDEX" } return "DROP" case strings.HasPrefix(upperSQL, "ALTER"): return "ALTER" case strings.HasPrefix(upperSQL, "TRUNCATE"): return "TRUNCATE" default: return "UNKNOWN" } } // getRowName 安全地获取行名,避免空指针引用 func getRowName(children []meta.DataMeta) string { if len(children) == 0 { return "" } if children[0].Value == nil { return "" } return fmt.Sprintf("%v", children[0].Value) } // DescribeUpdateTemplate 返回指定对象的修改模板,用于前端动态生成表单 func (q *MySQLDriver) DescribeUpdateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) { if len(path) == 0 { return meta.ObjectTemplate{}, fmt.Errorf("路径不能为空") } lastEntry := path[len(path)-1] objectType := strings.ToLower(lastEntry.Type) objectName := lastEntry.Name switch objectType { case "database": // 获取数据库的当前信息 currentValues, err := q.getCurrentDatabaseInfo(ctx, objectName) if err != nil { return meta.ObjectTemplate{}, err } tpl := meta.ObjectTemplate{ Operation: "update", ObjectType: "database", ParentHint: "", Fields: getDatabaseTemplateFields(true, currentValues), Current: currentValues, Example: map[string]string{"charset": "utf8mb4"}, } return tpl, nil case "table": if len(path) < 2 { return meta.ObjectTemplate{}, fmt.Errorf("表路径需要包含数据库信息") } dbName := path[0].Name tableName := objectName // 获取表的当前信息 currentValues, err := q.getCurrentTableInfo(ctx, dbName, tableName) if err != nil { return meta.ObjectTemplate{}, err } tpl := meta.ObjectTemplate{ Operation: "update", ObjectType: "table", ParentHint: "database: " + dbName, Fields: getTableTemplateFields(true, currentValues), Current: currentValues, Example: map[string]string{"engine": "InnoDB", "charset": "utf8mb4"}, } return tpl, nil default: return meta.ObjectTemplate{}, fmt.Errorf("不支持的 update 类型: %s", objectType) } } // UpdateObject 执行对象的修改操作 func (q *MySQLDriver) UpdateObject(ctx context.Context, req meta.UpdateObjectRequest) (meta.UpdateObjectResponse, error) { if len(req.Path) == 0 { return meta.UpdateObjectResponse{}, fmt.Errorf("路径不能为空") } lastEntry := req.Path[len(req.Path)-1] objectType := strings.ToLower(lastEntry.Type) objectName := lastEntry.Name var sqls []string switch objectType { case "database": dbName := objectName charset, charsetOk := req.Properties["charset"].(string) collation, collationOk := req.Properties["collation"].(string) if charsetOk && charset != "" { sql := fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET %s", dbName, charset) sqls = append(sqls, sql) } if collationOk && collation != "" { sql := fmt.Sprintf("ALTER DATABASE `%s` COLLATE %s", dbName, collation) sqls = append(sqls, sql) } case "table": if len(req.Path) < 2 { return meta.UpdateObjectResponse{}, fmt.Errorf("表路径需要包含数据库信息") } dbName := req.Path[0].Name tableName := objectName engine, engineOk := req.Properties["engine"].(string) charset, charsetOk := req.Properties["charset"].(string) collation, collationOk := req.Properties["collation"].(string) if engineOk && engine != "" { sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` ENGINE = %s", dbName, tableName, engine) sqls = append(sqls, sql) } if charsetOk && charset != "" { sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` CONVERT TO CHARACTER SET %s", dbName, tableName, charset) sqls = append(sqls, sql) } if collationOk && collation != "" { sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` COLLATE %s", dbName, tableName, collation) sqls = append(sqls, sql) } default: return meta.UpdateObjectResponse{}, fmt.Errorf("不支持的 update 类型: %s", objectType) } if !req.Execute { // 预览模式,只返回生成的SQL return meta.UpdateObjectResponse{ GeneratedSQL: sqls, }, nil } // 执行模式 for _, sql := range sqls { if _, err := q.db.ExecContext(ctx, sql); err != nil { return meta.UpdateObjectResponse{}, fmt.Errorf("执行SQL失败: %s, 错误: %v", sql, err) } } return meta.UpdateObjectResponse{ GeneratedSQL: sqls, }, nil } // DescribeDeleteTemplate 返回指定对象的删除模板,用于前端显示删除确认和影响预览 func (q *MySQLDriver) DescribeDeleteTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) { if len(path) == 0 { return meta.ObjectTemplate{}, fmt.Errorf("路径不能为空") } lastEntry := path[len(path)-1] objectType := strings.ToLower(lastEntry.Type) objectName := lastEntry.Name switch objectType { case "database": dbName := objectName // 查询数据库中的表数量 var tableCount int64 err := q.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?", dbName).Scan(&tableCount) if err != nil { return meta.ObjectTemplate{}, fmt.Errorf("获取数据库表数量失败: %w", err) } tpl := meta.ObjectTemplate{ Operation: "delete", ObjectType: "database", ParentHint: "", Current: map[string]string{ "databaseName": dbName, "tableCount": fmt.Sprintf("%d", tableCount), }, Fields: []meta.TemplateField{ {Name: "databaseName", Label: "Database Name", Type: meta.FieldTypeString, Required: true, Current: dbName, Editable: &[]bool{false}[0]}, {Name: "tableCount", Label: "Tables Count", Type: meta.FieldTypeString, Required: false, Current: fmt.Sprintf("%d", tableCount), Editable: &[]bool{false}[0]}, {Name: "confirmDelete", Label: "Confirm Delete", Type: meta.FieldTypeBool, Required: true, Help: "删除数据库将同时删除所有表和数据,此操作不可恢复"}, }, Notes: "警告:删除数据库将永久删除所有表、数据和相关对象。此操作不可恢复。", } return tpl, nil case "table": if len(path) < 2 { return meta.ObjectTemplate{}, fmt.Errorf("表路径需要包含数据库信息") } dbName := path[0].Name tableName := objectName // 查询表中的行数 var rowCount int64 query := fmt.Sprintf("SELECT COUNT(*) FROM `%s`.`%s`", dbName, tableName) err := q.db.QueryRowContext(ctx, query).Scan(&rowCount) if err != nil { // 如果查询失败,可能表不存在或权限问题 rowCount = -1 } tpl := meta.ObjectTemplate{ Operation: "delete", ObjectType: "table", ParentHint: "database: " + dbName, Current: map[string]string{ "tableName": tableName, "rowCount": fmt.Sprintf("%d", rowCount), }, Fields: []meta.TemplateField{ {Name: "tableName", Label: "Table Name", Type: meta.FieldTypeString, Required: true, Current: tableName, Editable: &[]bool{false}[0]}, {Name: "rowCount", Label: "Row Count", Type: meta.FieldTypeString, Required: false, Current: fmt.Sprintf("%d", rowCount), Editable: &[]bool{false}[0]}, {Name: "confirmDelete", Label: "Confirm Delete", Type: meta.FieldTypeBool, Required: true, Help: "删除表将永久删除所有数据,此操作不可恢复"}, }, Notes: "警告:删除表将永久删除所有数据。此操作不可恢复。", } return tpl, nil default: return meta.ObjectTemplate{}, fmt.Errorf("不支持的 delete 类型: %s", objectType) } }