package v8 import ( "context" "database/sql" "dbview/service/internal/common/databases/meta" "encoding/json" "fmt" "reflect" "sort" "strconv" "strings" "time" ) // 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 已移除,逻辑已内联到 ApplyChanges // deleteChildObjects 已移除,逻辑已内联到 ApplyChanges // 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 } // describeCreateTemplate 已内联到 GetObjectProperties,已删除以清理旧兼容层 // 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 已内联到 ApplyChanges,原 helper 已删除 // QueryData 执行数据查询(实现 DataReader 接口) func (q *MySQLDriver) QueryData(ctx context.Context, path meta.ObjectPath, req meta.DataQueryRequest, includeLarge bool) (meta.QueryResult, error) { startTime := time.Now() 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}, ExecutionTime: time.Since(startTime).Milliseconds(), }, 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 与 time.Time 类型;根据 includeLarge 决定是否返回或省略大型字段 omitted := false var sizeStr string if val != nil { switch v := val.(type) { case []byte: // 仅对数据库类型为 BLOB/TEXT/CLOB 等大字段在 includeLarge==false 时省略。 dbType := strings.ToUpper(columns[i].DBType) isLargeType := strings.Contains(dbType, "BLOB") || strings.Contains(dbType, "TEXT") || strings.Contains(dbType, "CLOB") || strings.Contains(dbType, "LONG") if includeLarge || !isLargeType { val = string(v) } else { omitted = true sizeStr = strconv.Itoa(len(v)) val = nil } case time.Time: val = v.Format(time.RFC3339) } } // 根据值类型设置 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 // 默认当作字符串 } } child := meta.DataMeta{ Name: columns[i].Name, Type: "value", Value: val, ValueType: valueType, } if omitted { if child.Meta == nil { child.Meta = map[string]string{} } child.Omitted = true child.Meta["size"] = sizeStr } children = append(children, child) } 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语句并返回执行结果 // includeLarge: 当为 true 时,驱动应尽量返回大型字段(BLOB/CLOB)的完整内容; // 当为 false 时,驱动可按策略省略大型字段并在返回的 DataMeta 中设置 `omitted:true`,以便前端按需加载。 func (q *MySQLDriver) ExecuteSQL(ctx context.Context, path meta.ObjectPath, sql string, params []interface{}, includeLarge bool) (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 // 若调用方传入了结构化路径且首段为 database,则在当前会话中切换到该数据库 // 这样用户可以在指定数据库上下文中运行任意 SQL(例如省略库名前缀的表名) if len(path) >= 1 && path[0].Name != "" { if _, err := db.ExecContext(ctx, fmt.Sprintf("USE `%s`", path[0].Name)); err != nil { result.ErrorMessage = fmt.Sprintf("切换数据库 `%s` 失败:%v", path[0].Name, err) result.ExecutionTime = time.Since(startTime).Milliseconds() return result, nil } } // 对于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] } // 获取列的数据库类型(如果可用)以便判断是否为大对象类型 dbType := "" if i < len(columnTypes) { dbType = columnTypes[i].DatabaseTypeName() } // 判断是否为大对象类型(简单规则:包含 BLOB/TEXT/CLOB/BINARY/VARBINARY 等) upperDBType := strings.ToUpper(dbType) isLargeDB := strings.Contains(upperDBType, "BLOB") || strings.Contains(upperDBType, "TEXT") || strings.Contains(upperDBType, "CLOB") || strings.Contains(upperDBType, "BINARY") || strings.Contains(upperDBType, "VARBINARY") // 根据值的实际类型决定最终的 Value 字段与是否在响应中省略 var value interface{} var omitted bool var metaMap map[string]string if val == nil { value = nil } else { switch v := val.(type) { case []byte: // 对于 []byte,根据 includeLarge 决定是否返回完整内容。 // - includeLarge == true: 返回字符串形式的内容 // - includeLarge == false: 若列类型为大对象或长度超过阈值(1KB),则省略并标记 omitted if includeLarge { value = string(v) } else if isLargeDB || len(v) > 1024 { omitted = true value = nil metaMap = map[string]string{"size": fmt.Sprintf("%d", len(v))} } else { value = string(v) } case time.Time: value = v.Format(time.RFC3339Nano) default: value = fmt.Sprintf("%v", v) } } dm := meta.DataMeta{ Name: colName, Type: "value", Value: value, DBType: dbType, Omitted: omitted, } if len(metaMap) > 0 { dm.Meta = metaMap } rowValues = append(rowValues, dm) } resultRows = append(resultRows, meta.DataMeta{ Type: "row", RowID: "", Children: rowValues, }) rowCount++ } // 构建查询结果 queryResult := &meta.QueryResult{ Columns: columnMetas, Rows: resultRows, Returned: len(resultRows), Truncated: rowCount >= 1000, ExecutionTime: time.Since(startTime).Milliseconds(), } 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) } // buildCreateTemplate 根据对象类型和父级名称构建创建模板(Create ObjectTemplate) func (q *MySQLDriver) buildCreateTemplate(ctx context.Context, objectType, parentName string) (meta.ObjectTemplate, error) { switch objectType { case "database": ex := map[string]string{"databaseName": "mydb", "charset": "utf8mb4", "ifNotExists": "true"} return meta.ObjectTemplate{ Operation: "create", ObjectType: "database", ParentHint: "", Fields: getDatabaseTemplateFields(false, nil), Example: ex, }, nil case "table": parentHint := "parentName should be the database name" if parentName != "" { parentHint = "database: " + parentName } 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"} return meta.ObjectTemplate{ Operation: "create", ObjectType: "table", ParentHint: parentHint, Fields: getTableTemplateFields(false, nil), Example: ex, }, 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"} return meta.ObjectTemplate{ Operation: "create", ObjectType: "index", ParentHint: parentHint, Fields: getIndexTemplateFields(false, nil), Example: ex, }, nil default: return meta.ObjectTemplate{}, fmt.Errorf("不支持的 create 类型: %s", objectType) } } // buildUpdateTemplate 为指定路径构建 update 模板(读取当前值并转换为 ObjectTemplate) func (q *MySQLDriver) buildUpdateTemplate(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 } return meta.ObjectTemplate{ Operation: "update", ObjectType: "database", ParentHint: "", Fields: getDatabaseTemplateFields(true, currentValues), Current: currentValues, Example: map[string]string{"charset": "utf8mb4"}, }, 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 } return meta.ObjectTemplate{ Operation: "update", ObjectType: "table", ParentHint: "database: " + dbName, Fields: getTableTemplateFields(true, currentValues), Current: currentValues, Example: map[string]string{"engine": "InnoDB", "charset": "utf8mb4"}, }, nil default: return meta.ObjectTemplate{}, fmt.Errorf("不支持的 update 类型: %s", objectType) } } // describeUpdateTemplate 已内联到 GetObjectProperties 并删除 // updateObject 已内联到 ApplyChanges 并删除 // describeDeleteTemplate 已内联到 GetObjectProperties / ApplyChanges,并已删除 // --- 适配器方法:实现新的 MetadataReader / ObjectManager 接口 --- // GetStructureDefinition 返回 MySQL 的节点类型定义 // 这些定义用于上层构建树状导航(例如数据库->表->列),包含类型标识、显示标签、图标与允许的子类型 func (q *MySQLDriver) GetStructureDefinition(ctx context.Context) ([]meta.NodeTypeDefinition, error) { defs := []meta.NodeTypeDefinition{ {Type: "database", Label: "Database", Icon: "database", ChildTypes: []string{"table", "view", "procedure", "trigger", "index"}, IsLeaf: false}, {Type: "table", Label: "Table", Icon: "table", ChildTypes: []string{"column", "index"}, IsLeaf: false}, {Type: "view", Label: "View", Icon: "eye", ChildTypes: []string{"column"}, IsLeaf: false}, {Type: "column", Label: "Column", Icon: "column", ChildTypes: nil, IsLeaf: true}, } return defs, nil } // 辅助函数:将内部通用对象 meta.GenericObject 转换为通用节点 meta.Node // - basePath: 如果提供,将在当前对象之前作为路径前缀 // - 该函数不会访问数据库,仅基于传入的 GenericObject 构造 Node,用于适配层返回统一模型 func genericToNode(g meta.GenericObject, basePath meta.ObjectPath) meta.Node { // 构建路径:复制 basePath 并在末尾追加当前对象的路径段 var p meta.ObjectPath if basePath != nil { p = append(meta.ObjectPath{}, basePath...) } // 将当前对象作为最后一个路径段追加 p = append(p, meta.ObjectPathEntry{Type: g.Type, Name: g.Name}) // 将 GenericObject 的 Attrs 复制到 Properties(保持简单的 key->value 映射) props := make(map[string]interface{}) for k, v := range g.Attrs { props[k] = v } return meta.Node{ Name: g.Name, Type: g.Type, Path: p, HasChildren: len(g.Children) > 0, Properties: props, } } // ListNodes 实现了 MetadataReader.ListNodes,内部委托到已有的 getRootObjects/getChildObjects // - path: 结构化路径,空路径表示根节点(返回数据库列表) // - req: 支持分页和按类型过滤(TypeFilter) func (q *MySQLDriver) ListNodes(ctx context.Context, path meta.ObjectPath, req meta.ListNodesRequest) ([]meta.Node, int64, error) { page := 1 pageSize := 100 if req.Page > 0 { page = req.Page } if req.PageSize > 0 { pageSize = req.PageSize } if len(path) == 0 { // root: databases 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 query := fmt.Sprintf( "SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA LIMIT %d OFFSET %d", pageSize, offset) // 只返回类型占位 if !true { // fetch is always true in ListNodes usage types := []meta.GenericObject{{ ID: fmt.Sprintf("%s.type-database", q.Name), Name: "database", Description: "数据库(root)类型,占位用于导航;前端可据此请求数据库下的子类型/条目", Type: "database", ParentID: q.Name, DBType: "mysql", Attrs: map[string]string{}, }} var nodes []meta.Node for _, g := range types { nodes = append(nodes, genericToNode(g, path)) } return nodes, 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", q.Name, page, pageSize, query, err) } defer rows.Close() var gos []meta.GenericObject for rows.Next() { var dbName sql.NullString var charset sql.NullString if err := rows.Scan(&dbName, &charset); err != nil { continue } gos = append(gos, meta.GenericObject{ ID: fmt.Sprintf("%s.db-%s", q.Name, dbName.String), Name: dbName.String, Type: "database", ParentID: q.Name, DBType: "mysql", Attrs: map[string]string{ "charset": charset.String, "createTime": "", }, Children: []meta.GenericObject{}, }) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("遍历 schema 结果集出错:%w", err) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, total, nil } // 在 path 的最后一项下列出子对象 childType := "" if len(req.TypeFilter) > 0 { childType = req.TypeFilter[0] } // inline getChildObjects logic db := q.db if len(path) == 0 { return nil, 0, fmt.Errorf("empty path") } parentEntry := path[len(path)-1] dbName := parentEntry.Name objectType := strings.ToLower(childType) if objectType == "" { objectType = "table" } switch objectType { case "table": tableType := "BASE 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) } 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 gos []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 } gos = append(gos, meta.GenericObject{ ID: fmt.Sprintf("%s.table-%s", dbName, tName.String), Name: tName.String, Type: "table", 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) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, total, nil case "view": tableType := "VIEW" 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) } 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 gos []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 } gos = append(gos, meta.GenericObject{ ID: fmt.Sprintf("%s.table-%s", dbName, tName.String), Name: tName.String, Type: "view", 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) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, total, nil case "index": 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 gos []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) gos = append(gos, 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) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, 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 gos []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) gos = append(gos, 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) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, total, nil case "trigger": 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 gos []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) gos = append(gos, 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) } var nodes []meta.Node for _, g := range gos { nodes = append(nodes, genericToNode(g, path)) } return nodes, total, nil default: return nil, 0, fmt.Errorf("不支持的类型: %s", objectType) } } // GetNodeDetails 将内部的 GetObjectDetails 结果映射为 NodeDetails 供上层使用 // - path: 目标对象的结构化路径 func (q *MySQLDriver) GetNodeDetails(ctx context.Context, path meta.ObjectPath) (meta.NodeDetails, error) { // 复用已有的 getObjectDetails 来获取完整的 GenericObject(含子对象) g, err := q.getObjectDetails(ctx, path, true) if err != nil { return meta.NodeDetails{}, err } node := genericToNode(g, path[:len(path)-1]) // 将一些原始属性填入 RawMetadata,便于上层展示或调试 raw := map[string]interface{}{ "attrs": g.Attrs, "child_count": len(g.Children), } return meta.NodeDetails{Node: node, RawMetadata: raw}, nil } // SearchNodes 提供基本的节点搜索实现:根据传入的作用域(scope)在数据库名或表名中搜索 // - 如果 scope 为空:在数据库名中搜索(返回数据库节点) // - 如果 scope 指向某个数据库:在该数据库下搜索表名 func (q *MySQLDriver) SearchNodes(ctx context.Context, keyword string, scope meta.ObjectPath) ([]meta.Node, error) { db := q.db kw := "%" + strings.ReplaceAll(keyword, "%", "\\%") + "%" // 如果 scope 为空,则在数据库名称中搜索 if len(scope) == 0 { rows, err := db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE ? LIMIT 100`, kw) if err != nil { return nil, err } defer rows.Close() var res []meta.Node for rows.Next() { var name sql.NullString var cs sql.NullString if err := rows.Scan(&name, &cs); err != nil { continue } g := meta.GenericObject{Name: name.String, Type: "database", Attrs: map[string]string{"charset": cs.String}} res = append(res, genericToNode(g, nil)) } return res, nil } // 如果 scope 指向一个数据库,则在该数据库的表名中搜索 if len(scope) >= 1 && strings.ToLower(scope[len(scope)-1].Type) == "database" { dbName := scope[len(scope)-1].Name rows, err := db.QueryContext(ctx, `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ? LIMIT 200`, dbName, kw) if err != nil { return nil, err } defer rows.Close() var res []meta.Node for rows.Next() { var t sql.NullString if err := rows.Scan(&t); err != nil { continue } g := meta.GenericObject{Name: t.String, Type: "table", ParentID: dbName} res = append(res, genericToNode(g, scope)) } return res, nil } return nil, nil } // ObjectManager: GetObjectProperties 将 DescribeCreateTemplate/DescribeUpdateTemplate 映射为 PropertyDefinitions // - 返回对前端友好的属性定义列表(PropertyDefinitions)以及当前值(PropertyValues),供表单渲染与预览使用 func (q *MySQLDriver) GetObjectProperties(ctx context.Context, path meta.ObjectPath) (meta.PropertyDefinitions, meta.PropertyValues, error) { // 根据 path 的最后一项是否包含 Name 字段决定是创建模板还是更新模板 if len(path) == 0 { return nil, nil, fmt.Errorf("path required") } last := path[len(path)-1] var tpl meta.ObjectTemplate var err error if last.Name == "" { // create 模板 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 nil, nil, fmt.Errorf("创建模板需要指定对象类型路径") } tpl, err = q.buildCreateTemplate(ctx, objectType, parentName) if err != nil { return nil, nil, err } } else { // update 模板 tpl, err = q.buildUpdateTemplate(ctx, path) if err != nil { return nil, nil, err } } // 将 ObjectTemplate 中的 TemplateField 转换为通用的 PropertyDefinition,并收集 Current 值到 PropertyValues var defs meta.PropertyDefinitions curr := make(meta.PropertyValues) for _, f := range tpl.Fields { pd := meta.PropertyDefinition{ Key: f.Name, Label: f.Label, Type: string(f.Type), Required: f.Required, Description: f.Help, ReadOnly: false, } if f.Default != "" { pd.DefaultValue = f.Default } if len(f.EnumOptions) > 0 { pd.Options = f.EnumOptions } defs = append(defs, pd) if f.Current != nil { curr[f.Name] = f.Current } } // also copy tmpl.Current (map[string]string) into curr for k, v := range tpl.Current { curr[k] = v } return defs, curr, nil } // applyCreate 处理 create 操作的预览与执行逻辑(从原 ApplyChanges 中抽出) func (q *MySQLDriver) applyCreate(ctx context.Context, path meta.ObjectPath, props meta.PropertyValues, execute bool) (meta.ApplyResult, error) { t := strings.ToLower(path[len(path)-1].Type) parentName := "" if len(path) >= 2 { parentName = path[len(path)-2].Name } var generated []string switch t { case "database": nameI, ok := props["databaseName"] if !ok { return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: databaseName") } name, _ := nameI.(string) if name == "" { return meta.ApplyResult{}, fmt.Errorf("databaseName 不能为空") } if execute { return meta.ApplyResult{}, 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 } sqlStr := "CREATE DATABASE" if ifNot { sqlStr += " IF NOT EXISTS" } sqlStr = fmt.Sprintf("%s `%s` DEFAULT CHARACTER SET = %s;", sqlStr, name, charset) generated = []string{sqlStr} case "table": tnameI, ok := props["tableName"] if !ok { return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: tableName") } tname, _ := tnameI.(string) if tname == "" { return meta.ApplyResult{}, fmt.Errorf("tableName 不能为空") } colsI, ok := props["columns"] if !ok { return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: columns") } colsSlice, ok := colsI.([]interface{}) if !ok { return meta.ApplyResult{}, fmt.Errorf("columns 格式无效,期望为数组类型的列定义,例如 [{\"name\":\"id\",\"type\":\"INT\"}]") } if execute { return meta.ApplyResult{}, 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 } 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 == "" { 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 { 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.ApplyResult{}, fmt.Errorf("未找到有效的列定义,请检查 properties.columns 字段,示例格式: [{\"name\":\"id\",\"type\":\"INT\"}]") } createStmt := "CREATE TABLE" if ifNot { createStmt += " IF NOT EXISTS" } if parentName != "" { createStmt = fmt.Sprintf("%s `%s`.`%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, 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) } generated = []string{createStmt} case "index": iname, _ := props["indexName"].(string) cols, _ := props["columns"].(string) unique := false if u, ok := props["unique"].(bool); ok { unique = u } if iname == "" || cols == "" { return meta.ApplyResult{}, fmt.Errorf("indexName 与 columns 为必填字段") } if execute { return meta.ApplyResult{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)") } uq := "" if unique { uq = "UNIQUE " } tableRef := iname if parentName != "" { tableRef = parentName } stmt := fmt.Sprintf("CREATE %sINDEX `%s` ON `%s` (%s);", uq, iname, tableRef, cols) generated = []string{stmt} default: return meta.ApplyResult{}, fmt.Errorf("不支持的 create 类型: %s", t) } var script string if len(generated) > 0 { script = strings.Join(generated, "\n") } return meta.ApplyResult{Script: script, Message: "ok"}, nil } // applyUpdate 处理 update 操作(生成 SQL 或执行) func (q *MySQLDriver) applyUpdate(ctx context.Context, path meta.ObjectPath, changes meta.PropertyValues, execute bool) (meta.ApplyResult, error) { if len(path) == 0 { return meta.ApplyResult{}, fmt.Errorf("path required for update") } last := path[len(path)-1] objectType := strings.ToLower(last.Type) objectName := last.Name var sqls []string switch objectType { case "database": charset, _ := changes["charset"].(string) collation, _ := changes["collation"].(string) if charset != "" { sqls = append(sqls, fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET %s", objectName, charset)) } if collation != "" { sqls = append(sqls, fmt.Sprintf("ALTER DATABASE `%s` COLLATE %s", objectName, collation)) } case "table": if len(path) < 2 { return meta.ApplyResult{}, fmt.Errorf("表路径需要包含数据库信息") } dbName := path[0].Name tableName := objectName engine, _ := changes["engine"].(string) charset, _ := changes["charset"].(string) collation, _ := changes["collation"].(string) if engine != "" { sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` ENGINE = %s", dbName, tableName, engine)) } if charset != "" { sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` CONVERT TO CHARACTER SET %s", dbName, tableName, charset)) } if collation != "" { sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` COLLATE %s", dbName, tableName, collation)) } default: return meta.ApplyResult{}, fmt.Errorf("不支持的 update 类型: %s", objectType) } if !execute { return meta.ApplyResult{Script: strings.Join(sqls, "\n"), Message: "ok"}, nil } for _, s := range sqls { if _, err := q.db.ExecContext(ctx, s); err != nil { return meta.ApplyResult{}, fmt.Errorf("执行SQL失败: %s, 错误: %v", s, err) } } return meta.ApplyResult{Script: strings.Join(sqls, "\n"), Message: "ok"}, nil } // applyDelete 处理 delete 操作(包括 root 与 child 删除逻辑) func (q *MySQLDriver) applyDelete(ctx context.Context, path meta.ObjectPath, execute bool) (meta.ApplyResult, error) { if len(path) == 0 { return meta.ApplyResult{}, fmt.Errorf("path required for delete") } last := path[len(path)-1] obj := meta.GenericObject{Type: last.Type, Name: last.Name} // root 删除(如数据库) if len(path) == 1 { db := q.db rootName := obj.Name typeName := obj.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 meta.ApplyResult{}, 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, }, }) } matchMap := map[string]interface{}{"matches": objs} b, _ := json.Marshal(matchMap) if !execute { return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil } tx, err := db.BeginTx(ctx, nil) if err != nil { return meta.ApplyResult{}, fmt.Errorf("开始事务失败:%w", err) } var execCount int64 var execSQLs []string for _, o := range objs { switch o.Type { case "database": sqlStr := fmt.Sprintf("DROP DATABASE `%s`", o.Name) if _, err := tx.ExecContext(ctx, sqlStr); err != nil { _ = tx.Rollback() return meta.ApplyResult{}, fmt.Errorf("执行 SQL 失败:%s, err: %w", sqlStr, err) } execSQLs = append(execSQLs, sqlStr) execCount++ default: } } if err := tx.Commit(); err != nil { _ = tx.Rollback() return meta.ApplyResult{}, fmt.Errorf("提交事务失败:%w", err) } if b, err := json.Marshal(execSQLs); err == nil { return meta.ApplyResult{Script: string(b), AffectedRows: execCount}, nil } return meta.ApplyResult{Script: "", AffectedRows: execCount}, 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 meta.ApplyResult{}, 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, }, }) } matchMap := map[string]interface{}{"matches": objs} if b, err := json.Marshal(matchMap); err == nil { return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil } return meta.ApplyResult{Script: "", AffectedRows: total}, nil default: return meta.ApplyResult{}, fmt.Errorf("不支持的 root 类型: %s", typeName) } } // child 删除逻辑 parentID := fmt.Sprintf("db-%s", path[len(path)-2].Name) filter := map[string]string{} if obj.Name != "" { filter["name"] = obj.Name } if obj.Type != "" { filter["type"] = obj.Type } 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 meta.ApplyResult{}, 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 b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil { return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil } return meta.ApplyResult{Script: "", AffectedRows: total}, 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 meta.ApplyResult{}, 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 b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil { return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil } return meta.ApplyResult{Script: "", AffectedRows: total}, 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 meta.ApplyResult{}, 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 b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil { return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil } return meta.ApplyResult{Script: "", AffectedRows: total}, nil default: return meta.ApplyResult{}, fmt.Errorf("不支持的 child 类型: %s", objectType) } } // ApplyChanges 实现对象的创建/更新/删除操作,内部委托到已有的 createObject/updateObject/delete* helper // - action: create/update/delete // - options.DryRun=true 时仅预览(不执行),否则执行对应操作 // 说明:为了兼容旧调用约定,`options.DryRun=true` 等价于旧接口中的 `Execute=false`。 // 驱动实现应使用 `options.DryRun` 或 `meta.ApplyOptions` 中的明确字段决定是否真正执行 SQL,而不是依赖旧的 Execute 标志。 func (q *MySQLDriver) ApplyChanges(ctx context.Context, action meta.ObjectAction, path meta.ObjectPath, changes meta.PropertyValues, options meta.ApplyOptions) (meta.ApplyResult, error) { // 注意:DryRun=true 对应 Execute=false(旧接口中 Execute 控制是否执行 SQL) execute := !options.DryRun switch action { case meta.ObjectAction("create"): return q.applyCreate(ctx, path, changes, execute) case meta.ObjectAction("update"): return q.applyUpdate(ctx, path, changes, execute) case meta.ObjectAction("delete"): return q.applyDelete(ctx, path, execute) default: return meta.ApplyResult{}, fmt.Errorf("unsupported action: %s", action) } }