Files
MyGoNavi/internal/app/methods_db_objects.go
Syngnat f55a332ead feat(mcp): 扩展数据库对象结构读取工具
- 新增 get_views/get_objects 工具并让 get_tables 附带视图列表
- 后端统一返回表、视图、触发器、函数、过程、序列、包、事件和队列类对象
- 补充 MCP 服务测试、对象解析测试、远程 schema-only 文档和多语言文案
2026-06-29 10:25:37 +08:00

489 lines
20 KiB
Go

package app
import (
"fmt"
"sort"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"GoNavi-Wails/internal/logger"
)
type objectMetadataQuerySpec struct {
sql string
inferredType string
}
func (a *App) DBGetObjects(config connection.ConnectionConfig, dbName string) connection.QueryResult {
runConfig := normalizeRunConfig(config, dbName)
dbType := resolveDDLDBType(runConfig)
if strings.EqualFold(strings.TrimSpace(runConfig.Type), "redis") {
keys := a.DBGetTables(config, dbName)
if !keys.Success {
return keys
}
names, err := decodeStringRows(keys.Data, "Table", "table", "name")
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: buildNamedObjects(dbName, "key", names)}
}
dbInst, err := a.getDatabase(runConfig)
if err != nil {
logger.Error(err, "DBGetObjects 获取连接失败:%s", formatConnSummary(runConfig))
return connection.QueryResult{Success: false, Message: err.Error()}
}
objects := make([]connection.DatabaseObject, 0, 128)
tableType := tableObjectTypeForDB(dbType)
if tables, tableErr := dbInst.GetTables(dbName); tableErr == nil {
objects = append(objects, buildNamedObjects(dbName, tableType, tables)...)
} else {
logger.Warnf("DBGetObjects 获取基础对象失败:%s err=%v", formatConnSummary(runConfig), tableErr)
}
if dbType == "rabbitmq" {
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "exchange", buildMessageExchangeMetadataQueries(dbType))...)
}
viewLookup := listViewNameLookup(dbInst, runConfig, dbName)
objects = append(objects, buildNamedObjects(dbName, "view", mapValuesSorted(viewLookup))...)
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "materialized_view", buildMaterializedViewMetadataQueries(dbType, dbName))...)
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "trigger", buildObjectTriggerMetadataQueries(dbType, dbName))...)
objects = append(objects, listRoutineObjects(dbInst, runConfig, dbName, buildObjectRoutineMetadataQueries(dbType, dbName))...)
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "sequence", buildObjectSequenceMetadataQueries(dbType, dbName))...)
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "package", buildObjectPackageMetadataQueries(dbType, dbName))...)
objects = append(objects, listObjectsByQueries(dbInst, runConfig, dbName, "event", buildObjectEventMetadataQueries(dbType, dbName))...)
return connection.QueryResult{Success: true, Data: dedupeSortDatabaseObjects(objects)}
}
func tableObjectTypeForDB(dbType string) string {
switch strings.ToLower(strings.TrimSpace(dbType)) {
case "rabbitmq":
return "queue"
case "rocketmq", "kafka", "mqtt":
return "topic"
default:
return "table"
}
}
func buildNamedObjects(dbName string, objectType string, names []string) []connection.DatabaseObject {
objects := make([]connection.DatabaseObject, 0, len(names))
for _, rawName := range names {
schema, name := "", strings.TrimSpace(rawName)
if shouldSplitDatabaseObjectName(objectType) {
schema, name = splitObjectSchemaName(rawName)
}
if strings.TrimSpace(name) == "" {
continue
}
objects = append(objects, connection.DatabaseObject{
Database: strings.TrimSpace(dbName),
Schema: strings.TrimSpace(schema),
Name: strings.TrimSpace(name),
Type: strings.TrimSpace(objectType),
})
}
return objects
}
func listObjectsByQueries(dbInst db.Database, config connection.ConnectionConfig, dbName string, objectType string, specs []objectMetadataQuerySpec) []connection.DatabaseObject {
objects := make([]connection.DatabaseObject, 0)
for _, spec := range normalizeObjectMetadataQuerySpecs(specs) {
rows, _, err := queryDataForExport(dbInst, config, spec.sql)
if err != nil {
continue
}
for _, row := range rows {
object := databaseObjectFromRow(dbName, objectType, spec.inferredType, row)
if object.Name == "" {
continue
}
objects = append(objects, object)
}
}
return objects
}
func listRoutineObjects(dbInst db.Database, config connection.ConnectionConfig, dbName string, specs []objectMetadataQuerySpec) []connection.DatabaseObject {
objects := make([]connection.DatabaseObject, 0)
for _, spec := range normalizeObjectMetadataQuerySpecs(specs) {
rows, _, err := queryDataForExport(dbInst, config, spec.sql)
if err != nil {
continue
}
for _, row := range rows {
rawType := rowStringCI(row, "routine_type", "object_type", "type")
if rawType == "" {
rawType = spec.inferredType
}
objectType := "function"
if strings.Contains(strings.ToUpper(rawType), "PROC") {
objectType = "procedure"
}
object := databaseObjectFromRow(dbName, objectType, rawType, row)
if object.Name == "" {
continue
}
objects = append(objects, object)
}
}
return objects
}
func databaseObjectFromRow(dbName string, objectType string, rawType string, row map[string]interface{}) connection.DatabaseObject {
schema := rowStringCI(row, "schema_name", "schemaname", "owner", "table_schema", "event_schema", "sequence_owner", "db", "database")
name := rowStringCI(row, objectNameKeysForType(objectType)...)
if name == "" {
name = mysqlShowObjectName(row)
}
if name == "" {
name = firstRowString(row)
}
if shouldSplitDatabaseObjectName(objectType) {
parsedSchema, parsedName := splitObjectSchemaName(name)
if schema == "" {
schema = parsedSchema
}
if parsedName != "" {
name = parsedName
}
}
parent := rowStringCI(row, "parent", "table_name", "event_object_table", "tbl_name", "table")
if parentSchema, parentName := splitObjectSchemaName(parent); parentName != "" {
if parentSchema != "" && !strings.Contains(parent, ".") {
parent = qualifyTable(parentSchema, parentName)
} else {
parent = strings.TrimSpace(parent)
}
}
if rawType == "" {
rawType = rowStringCI(row, "raw_type", "table_type", "object_type", "routine_type", "type", "event_type", "status")
}
return connection.DatabaseObject{
Database: strings.TrimSpace(dbName),
Schema: strings.TrimSpace(schema),
Name: strings.TrimSpace(name),
Type: strings.TrimSpace(objectType),
Parent: strings.TrimSpace(parent),
RawType: strings.TrimSpace(rawType),
Comment: rowStringCI(row, "comment", "comments", "description", "table_comment"),
}
}
func shouldSplitDatabaseObjectName(objectType string) bool {
switch strings.ToLower(strings.TrimSpace(objectType)) {
case "topic", "queue", "exchange", "key":
return false
default:
return true
}
}
func objectNameKeysForType(objectType string) []string {
switch strings.ToLower(strings.TrimSpace(objectType)) {
case "view", "materialized_view":
return []string{"object_name", "view_name", "viewname", "table_name", "name"}
case "trigger":
return []string{"trigger_name", "triggername", "trigger", "object_name", "name"}
case "function", "procedure":
return []string{"routine_name", "object_name", "proname", "name"}
case "sequence":
return []string{"sequence_name", "sequencename", "object_name", "name"}
case "package":
return []string{"package_name", "packagename", "object_name", "name"}
case "event":
return []string{"event_name", "eventname", "object_name", "name", "event"}
case "topic":
return []string{"topic", "topic_name", "name"}
case "queue":
return []string{"queue", "queue_name", "name"}
case "exchange":
return []string{"exchange", "exchange_name", "exchange_display", "name"}
default:
return []string{"object_name", "table_name", "name"}
}
}
func splitObjectSchemaName(raw string) (string, string) {
text := strings.TrimSpace(raw)
if text == "" {
return "", ""
}
schema, name := db.SplitSQLQualifiedName(text)
if name != "" {
return strings.TrimSpace(schema), strings.TrimSpace(name)
}
parts := strings.SplitN(text, ".", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
}
return "", text
}
func dedupeSortDatabaseObjects(objects []connection.DatabaseObject) []connection.DatabaseObject {
seen := make(map[string]struct{}, len(objects))
result := make([]connection.DatabaseObject, 0, len(objects))
for _, object := range objects {
object.Type = strings.ToLower(strings.TrimSpace(object.Type))
object.Name = strings.TrimSpace(object.Name)
object.Schema = strings.TrimSpace(object.Schema)
object.Database = strings.TrimSpace(object.Database)
if object.Name == "" || object.Type == "" {
continue
}
key := strings.ToLower(strings.Join([]string{object.Database, object.Type, object.Schema, object.Name, object.Parent}, "\x00"))
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, object)
}
sort.SliceStable(result, func(i, j int) bool {
left := result[i]
right := result[j]
for _, pair := range [][2]string{
{left.Type, right.Type},
{left.Schema, right.Schema},
{left.Name, right.Name},
{left.Parent, right.Parent},
} {
a := strings.ToLower(pair[0])
b := strings.ToLower(pair[1])
if a != b {
return a < b
}
}
return false
})
return result
}
func normalizeObjectMetadataQuerySpecs(specs []objectMetadataQuerySpec) []objectMetadataQuerySpec {
seen := make(map[string]struct{}, len(specs))
result := make([]objectMetadataQuerySpec, 0, len(specs))
for _, spec := range specs {
sql := strings.TrimSpace(spec.sql)
if sql == "" {
continue
}
key := strings.ToLower(spec.inferredType) + "\x00" + sql
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, objectMetadataQuerySpec{sql: sql, inferredType: spec.inferredType})
}
return result
}
func rowStringCI(row map[string]interface{}, keys ...string) string {
if len(row) == 0 {
return ""
}
values := make(map[string]interface{}, len(row))
for key, value := range row {
values[strings.ToLower(strings.TrimSpace(key))] = value
}
for _, key := range keys {
if value, ok := values[strings.ToLower(strings.TrimSpace(key))]; ok && value != nil {
text := strings.TrimSpace(fmt.Sprint(value))
if text != "" && text != "<nil>" {
return text
}
}
}
return ""
}
func firstRowString(row map[string]interface{}) string {
for _, value := range row {
if value == nil {
continue
}
text := strings.TrimSpace(fmt.Sprint(value))
if text != "" && text != "<nil>" {
return text
}
}
return ""
}
func mysqlShowObjectName(row map[string]interface{}) string {
for key, value := range row {
lower := strings.ToLower(strings.TrimSpace(key))
if !strings.HasPrefix(lower, "tables_in_") && !strings.HasPrefix(lower, "exchanges_in_") {
continue
}
text := strings.TrimSpace(fmt.Sprint(value))
if text != "" && text != "<nil>" {
return text
}
}
return ""
}
func decodeStringRows(data interface{}, keys ...string) ([]string, error) {
switch items := data.(type) {
case nil:
return []string{}, nil
case []map[string]string:
result := make([]string, 0, len(items))
for _, item := range items {
for _, key := range keys {
if value := strings.TrimSpace(item[key]); value != "" {
result = append(result, value)
break
}
}
}
return result, nil
default:
return nil, fmt.Errorf("unsupported object rows payload %T", data)
}
}
func buildObjectRoutineMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
safeDbName := escapeSQLLiteral(dbName)
switch dbType {
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx":
functionStatusQuery := fmt.Sprintf("SHOW FUNCTION STATUS WHERE Db = '%s'", safeDbName)
procedureStatusQuery := fmt.Sprintf("SHOW PROCEDURE STATUS WHERE Db = '%s'", safeDbName)
if strings.TrimSpace(dbName) == "" {
functionStatusQuery = "SHOW FUNCTION STATUS WHERE Db = DATABASE()"
procedureStatusQuery = "SHOW PROCEDURE STATUS WHERE Db = DATABASE()"
}
return []objectMetadataQuerySpec{
{sql: fmt.Sprintf("SELECT ROUTINE_NAME AS routine_name, ROUTINE_TYPE AS routine_type, ROUTINE_SCHEMA AS schema_name FROM information_schema.routines WHERE %s ORDER BY ROUTINE_TYPE, ROUTINE_NAME", mysqlMetadataSchemaPredicate("ROUTINE_SCHEMA", dbName))},
{sql: functionStatusQuery, inferredType: "FUNCTION"},
{sql: procedureStatusQuery, inferredType: "PROCEDURE"},
}
case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb":
return []objectMetadataQuerySpec{
{sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, routine_type, p.proname`},
{sql: `SELECT r.routine_schema AS schema_name, r.routine_name AS routine_name, COALESCE(NULLIF(UPPER(r.routine_type), ''), 'FUNCTION') AS routine_type FROM information_schema.routines r WHERE r.routine_schema NOT IN ('pg_catalog', 'information_schema') AND r.routine_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY r.routine_schema, routine_type, r.routine_name`},
{sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, 'FUNCTION' AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, p.proname`},
}
case "sqlserver":
safeDB := quoteIdentByType("sqlserver", firstNonEmptyString(dbName, "master"))
return []objectMetadataQuerySpec{{sql: fmt.Sprintf(`SELECT s.name AS schema_name, o.name AS routine_name, CASE o.type WHEN 'P' THEN 'PROCEDURE' WHEN 'FN' THEN 'FUNCTION' WHEN 'IF' THEN 'FUNCTION' WHEN 'TF' THEN 'FUNCTION' END AS routine_type FROM %s.sys.objects o JOIN %s.sys.schemas s ON o.schema_id = s.schema_id WHERE o.type IN ('P','FN','IF','TF') ORDER BY o.type, s.name, o.name`, safeDB, safeDB)}}
case "oracle", "dameng":
if strings.TrimSpace(dbName) == "" {
return []objectMetadataQuerySpec{{sql: `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`}}
}
return []objectMetadataQuerySpec{{sql: fmt.Sprintf("SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '%s' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME", strings.ToUpper(safeDbName))}}
case "duckdb":
return []objectMetadataQuerySpec{{sql: `SELECT schema_name, function_name AS routine_name, 'FUNCTION' AS routine_type FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND COALESCE(macro_definition, '') <> '' ORDER BY schema_name, function_name`, inferredType: "FUNCTION"}}
default:
return nil
}
}
func buildObjectTriggerMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
safeDbName := escapeSQLLiteral(dbName)
switch dbType {
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx":
specs := []objectMetadataQuerySpec{
{sql: fmt.Sprintf("SELECT TRIGGER_NAME AS trigger_name, EVENT_OBJECT_TABLE AS table_name, TRIGGER_SCHEMA AS schema_name FROM information_schema.triggers WHERE %s ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME", mysqlMetadataSchemaPredicate("TRIGGER_SCHEMA", dbName))},
}
if strings.TrimSpace(dbName) != "" {
specs = append(specs, objectMetadataQuerySpec{sql: fmt.Sprintf("SHOW TRIGGERS FROM %s", quoteIdentByType("mysql", dbName))})
}
return append(specs, objectMetadataQuerySpec{sql: "SHOW TRIGGERS"})
case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb":
return []objectMetadataQuerySpec{{sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY event_object_schema, event_object_table, trigger_name`}}
case "sqlserver":
safeDB := quoteIdentByType("sqlserver", firstNonEmptyString(dbName, "master"))
return []objectMetadataQuerySpec{{sql: fmt.Sprintf(`SELECT s.name AS schema_name, t.name AS table_name, tr.name AS trigger_name FROM %s.sys.triggers tr JOIN %s.sys.tables t ON tr.parent_id = t.object_id JOIN %s.sys.schemas s ON t.schema_id = s.schema_id WHERE tr.parent_class = 1 ORDER BY s.name, t.name, tr.name`, safeDB, safeDB, safeDB)}}
case "oracle", "dameng":
if strings.TrimSpace(dbName) == "" {
return []objectMetadataQuerySpec{{sql: `SELECT TRIGGER_NAME AS trigger_name, TABLE_NAME AS table_name FROM USER_TRIGGERS ORDER BY TABLE_NAME, TRIGGER_NAME`}}
}
return []objectMetadataQuerySpec{{sql: fmt.Sprintf("SELECT OWNER AS schema_name, TABLE_NAME AS table_name, TRIGGER_NAME AS trigger_name FROM ALL_TRIGGERS WHERE OWNER = '%s' ORDER BY TABLE_NAME, TRIGGER_NAME", strings.ToUpper(safeDbName))}}
case "sqlite":
return []objectMetadataQuerySpec{{sql: "SELECT name AS trigger_name, tbl_name AS table_name FROM sqlite_master WHERE type = 'trigger' ORDER BY tbl_name, name"}}
default:
return nil
}
}
func buildObjectSequenceMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
safeDbName := escapeSQLLiteral(dbName)
switch dbType {
case "oracle", "dameng":
if strings.TrimSpace(dbName) == "" {
return []objectMetadataQuerySpec{{sql: "SELECT SEQUENCE_NAME AS sequence_name FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME"}}
}
return []objectMetadataQuerySpec{{sql: fmt.Sprintf("SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '%s' ORDER BY SEQUENCE_NAME", strings.ToUpper(safeDbName))}}
default:
return nil
}
}
func buildObjectPackageMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
safeDbName := escapeSQLLiteral(dbName)
switch dbType {
case "oracle", "dameng":
if strings.TrimSpace(dbName) == "" {
return []objectMetadataQuerySpec{{sql: "SELECT OBJECT_NAME AS package_name FROM USER_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME"}}
}
return []objectMetadataQuerySpec{{sql: fmt.Sprintf("SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = '%s' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME", strings.ToUpper(safeDbName))}}
default:
return nil
}
}
func buildObjectEventMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
if dbType != "mysql" && dbType != "mariadb" && dbType != "oceanbase" && dbType != "diros" && dbType != "starrocks" {
return nil
}
specs := []objectMetadataQuerySpec{
{sql: fmt.Sprintf("SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_TYPE AS event_type, STATUS AS status FROM information_schema.events WHERE %s ORDER BY EVENT_NAME", mysqlMetadataSchemaPredicate("EVENT_SCHEMA", dbName))},
}
if strings.TrimSpace(dbName) != "" {
specs = append(specs, objectMetadataQuerySpec{sql: fmt.Sprintf("SHOW EVENTS FROM %s", quoteIdentByType("mysql", dbName))})
}
return append(specs, objectMetadataQuerySpec{sql: "SHOW EVENTS"})
}
func buildMaterializedViewMetadataQueries(dbType string, dbName string) []objectMetadataQuerySpec {
if dbType != "starrocks" {
return nil
}
specs := []objectMetadataQuerySpec{
{sql: fmt.Sprintf("SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name FROM information_schema.tables WHERE %s AND UPPER(TABLE_TYPE) LIKE '%%MATERIALIZED%%' ORDER BY TABLE_NAME", mysqlMetadataSchemaPredicate("TABLE_SCHEMA", dbName)), inferredType: "MATERIALIZED VIEW"},
}
if strings.TrimSpace(dbName) != "" {
specs = append(specs, objectMetadataQuerySpec{sql: fmt.Sprintf("SHOW MATERIALIZED VIEWS FROM %s", quoteIdentByType("mysql", dbName)), inferredType: "MATERIALIZED VIEW"})
}
return append(specs, objectMetadataQuerySpec{sql: "SHOW MATERIALIZED VIEWS", inferredType: "MATERIALIZED VIEW"})
}
func buildMessageExchangeMetadataQueries(dbType string) []objectMetadataQuerySpec {
if dbType != "rabbitmq" {
return nil
}
return []objectMetadataQuerySpec{{sql: "SHOW EXCHANGES", inferredType: "EXCHANGE"}}
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func mysqlMetadataSchemaPredicate(column string, dbName string) string {
if strings.TrimSpace(dbName) == "" {
return fmt.Sprintf("%s = DATABASE()", column)
}
return fmt.Sprintf("%s = '%s'", column, escapeSQLLiteral(dbName))
}