|
|
@@ -210,343 +210,9 @@ func (q *MySQLDriver) getTableIndexes(ctx context.Context, dbName, tableName, ob
|
|
|
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
|
|
|
+// deleteRootObjects 已移除,逻辑已内联到 ApplyChanges
|
|
|
|
|
|
- default:
|
|
|
- return resp, fmt.Errorf("不支持的 child 类型: %s", objectType)
|
|
|
- }
|
|
|
-}
|
|
|
+// deleteChildObjects 已移除,逻辑已内联到 ApplyChanges
|
|
|
|
|
|
// DescribeCreateTemplate 返回创建指定类型对象的表单模板(供前端渲染)
|
|
|
// getDatabaseTemplateFields 返回数据库相关的模板字段
|
|
|
@@ -679,104 +345,42 @@ func (q *MySQLDriver) getCurrentTableInfo(ctx context.Context, dbName, tableName
|
|
|
}, 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
|
|
|
+// 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 {
|
|
|
- return meta.ObjectTemplate{}, fmt.Errorf("创建模板需要指定对象类型路径")
|
|
|
+ // 回退到一组常见类型
|
|
|
+ caps.FieldTypes = []string{"INT", "BIGINT", "VARCHAR", "TEXT", "DATETIME", "TIMESTAMP", "DATE", "CHAR", "FLOAT", "DOUBLE", "DECIMAL", "BOOLEAN"}
|
|
|
}
|
|
|
|
|
|
- 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)
|
|
|
- }
|
|
|
+ // 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(部分版本可用)
|
|
|
@@ -828,167 +432,7 @@ func (q *MySQLDriver) GetMetadataInfo(ctx context.Context) (meta.MetadataCapabil
|
|
|
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)
|
|
|
- }
|
|
|
-}
|
|
|
+// createObject 已内联到 ApplyChanges,原 helper 已删除
|
|
|
|
|
|
// QueryData 执行数据查询(实现 DataReader 接口)
|
|
|
func (q *MySQLDriver) QueryData(ctx context.Context, path meta.ObjectPath, req meta.DataQueryRequest, includeLarge bool) (meta.QueryResult, error) {
|
|
|
@@ -1482,33 +926,77 @@ func getRowName(children []meta.DataMeta) string {
|
|
|
return fmt.Sprintf("%v", children[0].Value)
|
|
|
}
|
|
|
|
|
|
-// DescribeUpdateTemplate 返回指定对象的修改模板,用于前端动态生成表单
|
|
|
-func (q *MySQLDriver) describeUpdateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) {
|
|
|
+// 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
|
|
|
}
|
|
|
-
|
|
|
- tpl := meta.ObjectTemplate{
|
|
|
+ return meta.ObjectTemplate{
|
|
|
Operation: "update",
|
|
|
ObjectType: "database",
|
|
|
ParentHint: "",
|
|
|
Fields: getDatabaseTemplateFields(true, currentValues),
|
|
|
Current: currentValues,
|
|
|
Example: map[string]string{"charset": "utf8mb4"},
|
|
|
- }
|
|
|
- return tpl, nil
|
|
|
+ }, nil
|
|
|
|
|
|
case "table":
|
|
|
if len(path) < 2 {
|
|
|
@@ -1516,194 +1004,43 @@ func (q *MySQLDriver) describeUpdateTemplate(ctx context.Context, path meta.Obje
|
|
|
}
|
|
|
dbName := path[0].Name
|
|
|
tableName := objectName
|
|
|
-
|
|
|
- // 获取表的当前信息
|
|
|
currentValues, err := q.getCurrentTableInfo(ctx, dbName, tableName)
|
|
|
if err != nil {
|
|
|
return meta.ObjectTemplate{}, err
|
|
|
}
|
|
|
-
|
|
|
- tpl := meta.ObjectTemplate{
|
|
|
+ return 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
|
|
|
+ }, 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
|
|
|
+// describeUpdateTemplate 已内联到 GetObjectProperties 并删除
|
|
|
|
|
|
- var sqls []string
|
|
|
+// updateObject 已内联到 ApplyChanges 并删除
|
|
|
|
|
|
- switch objectType {
|
|
|
- case "database":
|
|
|
- dbName := objectName
|
|
|
- charset, charsetOk := req.Properties["charset"].(string)
|
|
|
- collation, collationOk := req.Properties["collation"].(string)
|
|
|
+// describeDeleteTemplate 已内联到 GetObjectProperties / ApplyChanges,并已删除
|
|
|
|
|
|
- if charsetOk && charset != "" {
|
|
|
- sql := fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET %s", dbName, charset)
|
|
|
- sqls = append(sqls, sql)
|
|
|
- }
|
|
|
+// --- 适配器方法:实现新的 MetadataReader / ObjectManager 接口 ---
|
|
|
|
|
|
- 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)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// --- 适配器方法:实现新的 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
|
|
|
-}
|
|
|
+// 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: 如果提供,将在当前对象之前作为路径前缀
|
|
|
@@ -2160,12 +1497,28 @@ func (q *MySQLDriver) GetObjectProperties(ctx context.Context, path meta.ObjectP
|
|
|
var tpl meta.ObjectTemplate
|
|
|
var err error
|
|
|
if last.Name == "" {
|
|
|
- tpl, err = q.describeCreateTemplate(ctx, path)
|
|
|
+ // 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 {
|
|
|
- tpl, err = q.describeUpdateTemplate(ctx, path)
|
|
|
- }
|
|
|
- if err != nil {
|
|
|
- return nil, nil, err
|
|
|
+ // update 模板
|
|
|
+ tpl, err = q.buildUpdateTemplate(ctx, path)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, err
|
|
|
+ }
|
|
|
}
|
|
|
// 将 ObjectTemplate 中的 TemplateField 转换为通用的 PropertyDefinition,并收集 Current 值到 PropertyValues
|
|
|
var defs meta.PropertyDefinitions
|
|
|
@@ -2197,70 +1550,524 @@ func (q *MySQLDriver) GetObjectProperties(ctx context.Context, path meta.ObjectP
|
|
|
return defs, curr, nil
|
|
|
}
|
|
|
|
|
|
-// ApplyChanges 实现对象的创建/更新/删除操作,内部委托到已有的 createObject/updateObject/delete* helper
|
|
|
-// - action: create/update/delete
|
|
|
-// - options.DryRun=true 时仅预览(不执行),否则执行对应操作
|
|
|
-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"):
|
|
|
- // 使用已有的 createObject API 生成/执行创建语句
|
|
|
- req := meta.CreateObjectRequest{
|
|
|
- ObjectType: path[len(path)-1].Type,
|
|
|
- ParentName: "",
|
|
|
- Properties: changes,
|
|
|
- Execute: execute,
|
|
|
+// 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")
|
|
|
}
|
|
|
- if len(path) >= 2 {
|
|
|
- req.ParentName = path[len(path)-2].Name
|
|
|
+ name, _ := nameI.(string)
|
|
|
+ if name == "" {
|
|
|
+ return meta.ApplyResult{}, fmt.Errorf("databaseName 不能为空")
|
|
|
}
|
|
|
- resp, err := q.createObject(ctx, req)
|
|
|
- if err != nil {
|
|
|
- return meta.ApplyResult{}, err
|
|
|
+ if execute {
|
|
|
+ return meta.ApplyResult{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
|
|
|
}
|
|
|
- var script string
|
|
|
- if len(resp.GeneratedSQL) > 0 {
|
|
|
- script = strings.Join(resp.GeneratedSQL, "\n")
|
|
|
+ charset := "utf8mb4"
|
|
|
+ if cs, ok := props["charset"].(string); ok && cs != "" {
|
|
|
+ charset = cs
|
|
|
}
|
|
|
- return meta.ApplyResult{Script: script, Message: "ok"}, nil
|
|
|
- case meta.ObjectAction("update"):
|
|
|
- ureq := meta.UpdateObjectRequest{
|
|
|
- Path: path,
|
|
|
- Properties: changes,
|
|
|
- Execute: execute,
|
|
|
+ ifNot := false
|
|
|
+ if v, ok := props["ifNotExists"].(bool); ok {
|
|
|
+ ifNot = v
|
|
|
}
|
|
|
- uresp, err := q.updateObject(ctx, ureq)
|
|
|
- if err != nil {
|
|
|
- return meta.ApplyResult{}, err
|
|
|
+ sqlStr := "CREATE DATABASE"
|
|
|
+ if ifNot {
|
|
|
+ sqlStr += " IF NOT EXISTS"
|
|
|
}
|
|
|
- var script string
|
|
|
- if len(uresp.GeneratedSQL) > 0 {
|
|
|
- script = strings.Join(uresp.GeneratedSQL, "\n")
|
|
|
+ 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")
|
|
|
}
|
|
|
- return meta.ApplyResult{Script: script, Message: "ok"}, nil
|
|
|
- case meta.ObjectAction("delete"):
|
|
|
- // Build ObjectOperationRequest
|
|
|
- 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}
|
|
|
- doreq := meta.ObjectOperationRequest{Object: obj, Execute: execute}
|
|
|
- // If deleting root (database)
|
|
|
- if len(path) == 1 {
|
|
|
- resp, err := q.deleteRootObjects(ctx, doreq)
|
|
|
+ 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{}, err
|
|
|
+ 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: resp.Sql, AffectedRows: resp.Affected}, 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)
|
|
|
}
|
|
|
- // else child
|
|
|
- resp, err := q.deleteChildObjects(ctx, doreq)
|
|
|
if err != nil {
|
|
|
- return meta.ApplyResult{}, err
|
|
|
+ return meta.ApplyResult{}, fmt.Errorf("查询索引以供删除匹配失败:%w", err)
|
|
|
}
|
|
|
- return meta.ApplyResult{Script: resp.Sql, AffectedRows: resp.Affected}, nil
|
|
|
+ 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)
|
|
|
}
|