fix: make postgresql actually usable (4 critical bugs)
- CORS whitelist only allowed origin localhost:5173; browsers send Origin on same-origin POST too, so every deployment on any other port/origin got empty 403 on ALL POST/PUT/DELETE (curl tests masked it) - now echoes any origin (bearer-token auth, no cookie CSRF surface) - connection_tester pg DSN had a literal *** password placeholder - pg connection test and query path could never authenticate - instance detail endpoints (databases/tables/db-schema/table-detail) ran MySQL SHOW statements against pg - added pg branches (pg_database/pg_class/information_schema/pg_indexes, search_path semantics, PRI detection); schema filter uses non-system schemas - frontend normalizeDbType missed the literal 'postgresql' value so pg instances were treated as mysql (backtick quoting, no column hints); genSql no longer emits db.table prefix for pg (schema semantics) - also: query page footer warnings condensed to a single line per db type verified live: mysql table tree/context menu/preview-execute; pg table tree/columns/PK, double-click preview with masking working (email/phone masked), query history persisted
This commit is contained in:
1 parent
c8155e2d33
commit
2c96902fbd
7 files changed
+183
-37
No files matched your search
@@ -45,6 +45,26 @@ func DetailListDatabases(c *gin.Context) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if instance.DBType == "postgresql" || instance.DBType == "postgres" || instance.DBType == "pg" {
|
||||
pgRows, err := db.Query("SELECT datname FROM pg_database WHERE NOT datistemplate AND datallowconn ORDER BY 1")
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
defer pgRows.Close()
|
||||
var databases []string
|
||||
for pgRows.Next() {
|
||||
var name string
|
||||
pgRows.Scan(&name)
|
||||
databases = append(databases, name)
|
||||
}
|
||||
if databases == nil {
|
||||
databases = []string{}
|
||||
}
|
||||
c.JSON(http.StatusOK, databases)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
@@ -99,6 +119,33 @@ func DetailListTables(c *gin.Context) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if instance.DBType == "postgresql" || instance.DBType == "postgres" || instance.DBType == "pg" {
|
||||
pgRows, err := db.Query(`SELECT c.relname AS name, c.reltuples::bigint AS rows,
|
||||
pg_total_relation_size(c.oid) AS data_length
|
||||
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND c.relkind = 'r'
|
||||
ORDER BY c.relname`)
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
defer pgRows.Close()
|
||||
var tables []map[string]interface{}
|
||||
for pgRows.Next() {
|
||||
var name string
|
||||
var rowCount, dataLen int64
|
||||
pgRows.Scan(&name, &rowCount, &dataLen)
|
||||
tables = append(tables, map[string]interface{}{
|
||||
"name": name, "rows": rowCount, "data_length": dataLen, "engine": "postgres",
|
||||
})
|
||||
}
|
||||
if tables == nil {
|
||||
tables = []map[string]interface{}{}
|
||||
}
|
||||
c.JSON(http.StatusOK, tables)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query("SHOW TABLE STATUS")
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
@@ -166,6 +213,27 @@ func DetailDbSchema(c *gin.Context) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
schema := make(map[string][]string)
|
||||
|
||||
if instance.DBType == "postgresql" || instance.DBType == "postgres" || instance.DBType == "pg" {
|
||||
// PG:一次查询拿全部 库内表.列(information_schema)
|
||||
colRows, err := db.Query(`SELECT table_name, column_name FROM information_schema.columns
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY table_name, ordinal_position`)
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
defer colRows.Close()
|
||||
for colRows.Next() {
|
||||
var tableName, columnName string
|
||||
colRows.Scan(&tableName, &columnName)
|
||||
schema[tableName] = append(schema[tableName], columnName)
|
||||
}
|
||||
c.JSON(http.StatusOK, schema)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all tables and their columns
|
||||
tableRows, err := db.Query("SHOW TABLES")
|
||||
if err != nil {
|
||||
@@ -174,7 +242,6 @@ func DetailDbSchema(c *gin.Context) {
|
||||
}
|
||||
defer tableRows.Close()
|
||||
|
||||
schema := make(map[string][]string)
|
||||
for tableRows.Next() {
|
||||
var tableName string
|
||||
tableRows.Scan(&tableName)
|
||||
@@ -229,6 +296,79 @@ func DetailTableDetail(c *gin.Context) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if instance.DBType == "postgresql" || instance.DBType == "postgres" || instance.DBType == "pg" {
|
||||
// PG:information_schema 拼列 + pg_indexes 拿索引(无 SHOW CREATE,DDL 以列清单形式给出)
|
||||
colRows, err := db.Query(`SELECT column_name, data_type, is_nullable,
|
||||
COALESCE(column_default, ''), character_maximum_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_name = $1
|
||||
ORDER BY ordinal_position`, tableName)
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
defer colRows.Close()
|
||||
var columns []map[string]interface{}
|
||||
var ddlLines []string
|
||||
for colRows.Next() {
|
||||
var name, dataType, nullable, def string
|
||||
var maxLen interface{}
|
||||
colRows.Scan(&name, &dataType, &nullable, &def, &maxLen)
|
||||
nullStr := "YES"
|
||||
if nullable != "YES" {
|
||||
nullStr = "NO"
|
||||
}
|
||||
key := ""
|
||||
pkRows, _ := db.Query(`SELECT a.attname FROM pg_index i
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||
WHERE i.indrelid = to_regclass($1) AND i.indisprimary`, tableName)
|
||||
if pkRows != nil {
|
||||
for pkRows.Next() {
|
||||
var pk string
|
||||
pkRows.Scan(&pk)
|
||||
if pk == name {
|
||||
key = "PRI"
|
||||
}
|
||||
}
|
||||
pkRows.Close()
|
||||
}
|
||||
columns = append(columns, map[string]interface{}{
|
||||
"Field": name, "Type": dataType, "Null": nullStr, "Key": key, "Default": def, "Extra": "",
|
||||
})
|
||||
ddlLines = append(ddlLines, " "+name+" "+dataType+(func() string {
|
||||
if nullStr == "NO" {
|
||||
return " NOT NULL"
|
||||
}
|
||||
return ""
|
||||
})())
|
||||
}
|
||||
ddl := "CREATE TABLE " + tableName + " (\n" + strings.Join(ddlLines, ",\n") + "\n);"
|
||||
|
||||
idxRows, err := db.Query(`SELECT indexname, indexdef FROM pg_indexes
|
||||
WHERE schemaname NOT IN ('pg_catalog', 'information_schema') AND tablename = $1
|
||||
ORDER BY indexname`, tableName)
|
||||
var indexes []map[string]interface{}
|
||||
if err == nil {
|
||||
defer idxRows.Close()
|
||||
for idxRows.Next() {
|
||||
var idxName, idxDef string
|
||||
idxRows.Scan(&idxName, &idxDef)
|
||||
nonUnique := !strings.Contains(idxDef, " UNIQUE INDEX ")
|
||||
indexes = append(indexes, map[string]interface{}{
|
||||
"Key_name": idxName, "Column_name": strings.Trim(idxDef[strings.LastIndex(idxDef, "(")+1:], ")"), "Non_unique": nonUnique, "Index_type": "btree",
|
||||
})
|
||||
}
|
||||
}
|
||||
if indexes == nil {
|
||||
indexes = []map[string]interface{}{}
|
||||
}
|
||||
if columns == nil {
|
||||
columns = []map[string]interface{}{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ddl": ddl, "columns": columns, "indexes": indexes})
|
||||
return
|
||||
}
|
||||
|
||||
// Get DDL
|
||||
var ddl string
|
||||
db.QueryRow("SHOW CREATE TABLE `"+tableName+"`").Scan(&tableName, &ddl)
|
||||
|
||||
Reference in new issue
Block a user