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 != "" { 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 != "" { 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 != "" { 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 * FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME"}, {sql: "SELECT OBJECT_NAME AS sequence_name FROM USER_OBJECTS WHERE OBJECT_TYPE = 'SEQUENCE' ORDER BY OBJECT_NAME"}, } } owner := strings.ToUpper(safeDbName) return []objectMetadataQuerySpec{ {sql: fmt.Sprintf("SELECT * FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '%s' ORDER BY SEQUENCE_NAME", owner)}, {sql: fmt.Sprintf("SELECT OWNER AS schema_name, OBJECT_NAME AS sequence_name FROM ALL_OBJECTS WHERE OWNER = '%s' AND OBJECT_TYPE = 'SEQUENCE' ORDER BY OBJECT_NAME", owner)}, } 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)) }