| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074 |
- 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 "<NULL>"
- }
- 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)
- }
- }
|