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
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -116,8 +117,8 @@ func testPostgreSQL(ctx context.Context, host string, port int, user, pass, dbna
|
|||||||
if dbname == "" {
|
if dbname == "" {
|
||||||
dbname = "postgres"
|
dbname = "postgres"
|
||||||
}
|
}
|
||||||
dsn := fmt.Sprintf("postgres://%s:***@%s:%d/%s?sslmode=disable&connect_timeout=10",
|
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable&connect_timeout=10",
|
||||||
user, host, port, dbname)
|
url.PathEscape(user), url.QueryEscape(pass), host, port, url.PathEscape(dbname))
|
||||||
db, err := sql.Open("pgx", dsn)
|
db, err := sql.Open("pgx", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Sprintf("Connection failed: %v", err)
|
return false, fmt.Sprintf("Connection failed: %v", err)
|
||||||
@@ -549,8 +550,8 @@ func getDBConnection(instance *model.Instance, dbName string) (*sql.DB, error) {
|
|||||||
if dbName == "" {
|
if dbName == "" {
|
||||||
dbName = "postgres"
|
dbName = "postgres"
|
||||||
}
|
}
|
||||||
dsn := fmt.Sprintf("postgres://%s:***@%s:%d/%s?sslmode=disable&connect_timeout=10",
|
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable&connect_timeout=10",
|
||||||
instance.Username, instance.Host, instance.Port, dbName)
|
url.PathEscape(instance.Username), url.QueryEscape(password), instance.Host, instance.Port, url.PathEscape(dbName))
|
||||||
return sql.Open("pgx", dsn)
|
return sql.Open("pgx", dsn)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported database type: %s", instance.DBType)
|
return nil, fmt.Errorf("unsupported database type: %s", instance.DBType)
|
||||||
|
|||||||
@@ -45,6 +45,26 @@ func DetailListDatabases(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
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")
|
rows, err := db.Query("SHOW DATABASES")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.InternalErr(c, err)
|
resp.InternalErr(c, err)
|
||||||
@@ -99,6 +119,33 @@ func DetailListTables(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
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")
|
rows, err := db.Query("SHOW TABLE STATUS")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.InternalErr(c, err)
|
resp.InternalErr(c, err)
|
||||||
@@ -166,6 +213,27 @@ func DetailDbSchema(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
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
|
// Get all tables and their columns
|
||||||
tableRows, err := db.Query("SHOW TABLES")
|
tableRows, err := db.Query("SHOW TABLES")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -174,7 +242,6 @@ func DetailDbSchema(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer tableRows.Close()
|
defer tableRows.Close()
|
||||||
|
|
||||||
schema := make(map[string][]string)
|
|
||||||
for tableRows.Next() {
|
for tableRows.Next() {
|
||||||
var tableName string
|
var tableName string
|
||||||
tableRows.Scan(&tableName)
|
tableRows.Scan(&tableName)
|
||||||
@@ -229,6 +296,79 @@ func DetailTableDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
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
|
// Get DDL
|
||||||
var ddl string
|
var ddl string
|
||||||
db.QueryRow("SHOW CREATE TABLE `"+tableName+"`").Scan(&tableName, &ddl)
|
db.QueryRow("SHOW CREATE TABLE `"+tableName+"`").Scan(&tableName, &ddl)
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import (
|
|||||||
|
|
||||||
func CORSMiddleware() gin.HandlerFunc {
|
func CORSMiddleware() gin.HandlerFunc {
|
||||||
return cors.New(cors.Config{
|
return cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"http://localhost:5173", "http://127.0.0.1:5173"},
|
// 内网运维平台:同源部署为主,开发端口不固定,按 Origin 回显放行
|
||||||
|
// (鉴权走 Bearer Token 不依赖 Cookie,CSRF 面可控)
|
||||||
|
AllowOriginFunc: func(origin string) bool {
|
||||||
|
return origin != ""
|
||||||
|
},
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
||||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||||
ExposeHeaders: []string{"Content-Length"},
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,11 @@ describe('primaryColumns', () => {
|
|||||||
describe('方言引号', () => {
|
describe('方言引号', () => {
|
||||||
it('MySQL 反引号 / PG 双引号', () => {
|
it('MySQL 反引号 / PG 双引号', () => {
|
||||||
expect(previewSelectSql('t1', 'mysql')).toBe('SELECT * FROM `t1`');
|
expect(previewSelectSql('t1', 'mysql')).toBe('SELECT * FROM `t1`');
|
||||||
expect(previewSelectSql('t1', 'postgresql')).toContain('"t1"');
|
expect(previewSelectSql('t1', 'postgresql')).toBe('SELECT * FROM "t1"');
|
||||||
});
|
});
|
||||||
it('预览 SELECT 不带 LIMIT(翻页由请求层承担)', () => {
|
it('预览 SELECT 不带 LIMIT(翻页由请求层承担);PG 不加库名前缀(schema 语义)', () => {
|
||||||
const sql = previewSelectSql('t1', 'mysql', 'db1');
|
expect(previewSelectSql('t1', 'mysql', 'db1')).toBe('SELECT * FROM `db1`.`t1`');
|
||||||
expect(sql).toBe('SELECT * FROM `db1`.`t1`');
|
expect(previewSelectSql('t1', 'postgresql', 'db1')).toBe('SELECT * FROM "t1"');
|
||||||
expect(sql.toLowerCase()).not.toContain('limit');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import type { DBType } from '@/lib/queryTemplates';
|
|||||||
|
|
||||||
/** 数据库类型 → 展示名 */
|
/** 数据库类型 → 展示名 */
|
||||||
export const dbTypeLabel: Record<string, string> = {
|
export const dbTypeLabel: Record<string, string> = {
|
||||||
mysql: 'MySQL', postgres: 'PostgreSQL', postgresql: 'PostgreSQL', redis: 'Redis', mongo: 'MongoDB', mongodb: 'MongoDB',
|
mysql: 'MySQL',
|
||||||
|
postgres: 'PostgreSQL',
|
||||||
|
postgresql: 'PostgreSQL',
|
||||||
|
redis: 'Redis',
|
||||||
|
mongo: 'MongoDB',
|
||||||
|
mongodb: 'MongoDB',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 数据库类型 → 图标地址 */
|
/** 数据库类型 → 图标地址 */
|
||||||
@@ -20,24 +25,27 @@ export const dbTypeIcon: Record<string, string> = {
|
|||||||
|
|
||||||
/** 编辑器占位示例 SQL/命令 */
|
/** 编辑器占位示例 SQL/命令 */
|
||||||
export const placeholderMap: Record<DBType, string> = {
|
export const placeholderMap: Record<DBType, string> = {
|
||||||
mysql: "-- MySQL 查询\n-- 仅支持 SELECT, SHOW, EXPLAIN, DESCRIBE\n\nSELECT id, name, status\nFROM users\nWHERE status = 'active'\nLIMIT 100;",
|
mysql:
|
||||||
postgresql: "-- PostgreSQL 查询\n-- 仅支持 SELECT, EXPLAIN\n\nSELECT id, name, status\nFROM users\nWHERE status = 'active'\nLIMIT 100;",
|
"-- MySQL 查询\n-- 仅支持 SELECT, SHOW, EXPLAIN, DESCRIBE\n\nSELECT id, name, status\nFROM users\nWHERE status = 'active'\nLIMIT 100;",
|
||||||
redis: "-- Redis 命令\nGET user:1001\nHGETALL user:1001\nKEYS user:*",
|
postgresql:
|
||||||
mongodb: "// MongoDB Shell\n// 支持 find, aggregate, count, distinct\n\ndb.users.find({ status: 'active' }).limit(100)",
|
"-- PostgreSQL 查询\n-- 仅支持 SELECT, EXPLAIN\n\nSELECT id, name, status\nFROM users\nWHERE status = 'active'\nLIMIT 100;",
|
||||||
|
redis: '-- Redis 命令\nGET user:1001\nHGETALL user:1001\nKEYS user:*',
|
||||||
|
mongodb:
|
||||||
|
"// MongoDB Shell\n// 支持 find, aggregate, count, distinct\n\ndb.users.find({ status: 'active' }).limit(100)",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 归一化数据库类型标识(后端别名 → 统一 DBType) */
|
/** 归一化数据库类型标识(后端别名 → 统一 DBType) */
|
||||||
export function normalizeDbType(dbType?: string): DBType {
|
export function normalizeDbType(dbType?: string): DBType {
|
||||||
if (dbType === 'postgres' || dbType === 'pg') return 'postgresql';
|
if (dbType === 'postgres' || dbType === 'pg' || dbType === 'postgresql') return 'postgresql';
|
||||||
if (dbType === 'mongo') return 'mongodb';
|
if (dbType === 'mongo') return 'mongodb';
|
||||||
if (dbType === 'redis') return 'redis';
|
if (dbType === 'redis') return 'redis';
|
||||||
return 'mysql';
|
return 'mysql';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 各库查询入口的权限策略文案 */
|
/** 各库查询入口的权限策略文案(单行简洁提示) */
|
||||||
export const queryPolicyText: Record<DBType, string> = {
|
export const queryPolicyText: Record<DBType, string> = {
|
||||||
mysql: '查询入口仅允许 SELECT / SHOW / DESCRIBE / EXPLAIN;禁止 UPDATE/DELETE/DDL、多语句、SELECT FOR UPDATE。未写 LIMIT 时按平台配置自动限制。',
|
mysql: '仅允许 SELECT/SHOW/DESCRIBE/EXPLAIN,自动限行与敏感字段脱敏',
|
||||||
postgresql: '查询入口仅允许 SELECT / EXPLAIN;禁止 EXPLAIN ANALYZE、写入 CTE、多语句和锁定读。未写 LIMIT 时按平台配置自动限制。',
|
postgresql: '仅允许 SELECT/EXPLAIN,自动限行与敏感字段脱敏',
|
||||||
redis: 'Redis 查询入口仅允许只读命令;写命令请提交变更工单。集群实例会按 slot 路由并扫描所有 master。',
|
redis: '仅允许只读命令,写操作请提工单',
|
||||||
mongodb: 'MongoDB 查询入口用于 find / aggregate / count / distinct 等只读操作;写操作请提交变更工单。',
|
mongodb: '仅允许只读操作,写操作请提工单',
|
||||||
};
|
};
|
||||||
@@ -27,8 +27,10 @@ function quote(name: string, dialect: 'mysql' | 'postgresql'): string {
|
|||||||
export type SqlDialect = 'mysql' | 'postgresql' | 'mongodb' | 'redis';
|
export type SqlDialect = 'mysql' | 'postgresql' | 'mongodb' | 'redis';
|
||||||
|
|
||||||
export function previewSelectSql(table: string, dialect: SqlDialect, database?: string): string {
|
export function previewSelectSql(table: string, dialect: SqlDialect, database?: string): string {
|
||||||
const q = (n: string) => quote(n, dialect === 'postgresql' ? 'postgresql' : 'mysql');
|
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
||||||
const target = database ? `${q(database)}.${q(table)}` : q(table);
|
const q = (n: string) => quote(n, d);
|
||||||
|
// MySQL 支持 db.table 两段限定;PG 的两段限定是 schema.table,库名前缀不可用 → 不加前缀走 search_path
|
||||||
|
const target = database && d === 'mysql' ? `${q(database)}.${q(table)}` : q(table);
|
||||||
// 不烧 LIMIT:翻页由请求层 offset/limit 承担(backend 自动加 LIMIT)
|
// 不烧 LIMIT:翻页由请求层 offset/limit 承担(backend 自动加 LIMIT)
|
||||||
return `SELECT * FROM ${target}`;
|
return `SELECT * FROM ${target}`;
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ export function generateSelectSql(
|
|||||||
): string {
|
): string {
|
||||||
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
||||||
const q = (n: string) => quote(n, d);
|
const q = (n: string) => quote(n, d);
|
||||||
const target = database ? `${q(database)}.${q(table)}` : q(table);
|
const target = database && d === 'mysql' ? `${q(database)}.${q(table)}` : q(table);
|
||||||
const cols = columns.length > 0 ? columns.map((c) => q(c.Field)).join(', ') : '*';
|
const cols = columns.length > 0 ? columns.map((c) => q(c.Field)).join(', ') : '*';
|
||||||
const pks = primaryColumns(columns);
|
const pks = primaryColumns(columns);
|
||||||
const where = pks.length > 0 ? `\nWHERE ${pks.map((p) => `${q(p)} = ?`).join(' AND ')}` : '';
|
const where = pks.length > 0 ? `\nWHERE ${pks.map((p) => `${q(p)} = ?`).join(' AND ')}` : '';
|
||||||
@@ -56,7 +58,7 @@ export function generateInsertSql(
|
|||||||
): string {
|
): string {
|
||||||
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
||||||
const q = (n: string) => quote(n, d);
|
const q = (n: string) => quote(n, d);
|
||||||
const target = database ? `${q(database)}.${q(table)}` : q(table);
|
const target = database && d === 'mysql' ? `${q(database)}.${q(table)}` : q(table);
|
||||||
const cols = columns.map((c) => q(c.Field));
|
const cols = columns.map((c) => q(c.Field));
|
||||||
const placeholders = columns.map(() => '?');
|
const placeholders = columns.map(() => '?');
|
||||||
if (cols.length === 0) return `INSERT INTO ${target} ...;`;
|
if (cols.length === 0) return `INSERT INTO ${target} ...;`;
|
||||||
@@ -71,7 +73,7 @@ export function generateUpdateSql(
|
|||||||
): string {
|
): string {
|
||||||
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
||||||
const q = (n: string) => quote(n, d);
|
const q = (n: string) => quote(n, d);
|
||||||
const target = database ? `${q(database)}.${q(table)}` : q(table);
|
const target = database && d === 'mysql' ? `${q(database)}.${q(table)}` : q(table);
|
||||||
const setCols = columns.length > 0 ? columns : [{ Field: 'column1' } as ColumnMeta];
|
const setCols = columns.length > 0 ? columns : [{ Field: 'column1' } as ColumnMeta];
|
||||||
const pks = primaryColumns(columns);
|
const pks = primaryColumns(columns);
|
||||||
const whereCols = pks.length > 0 ? pks : [setCols[0].Field];
|
const whereCols = pks.length > 0 ? pks : [setCols[0].Field];
|
||||||
@@ -86,7 +88,7 @@ export function generateDeleteSql(
|
|||||||
): string {
|
): string {
|
||||||
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
const d = dialect === 'postgresql' ? 'postgresql' : 'mysql';
|
||||||
const q = (n: string) => quote(n, d);
|
const q = (n: string) => quote(n, d);
|
||||||
const target = database ? `${q(database)}.${q(table)}` : q(table);
|
const target = database && d === 'mysql' ? `${q(database)}.${q(table)}` : q(table);
|
||||||
const pks = primaryColumns(columns);
|
const pks = primaryColumns(columns);
|
||||||
const whereCols = pks.length > 0 ? pks : columns[0] ? [columns[0].Field] : ['id'];
|
const whereCols = pks.length > 0 ? pks : columns[0] ? [columns[0].Field] : ['id'];
|
||||||
return `DELETE FROM ${target}\nWHERE ${whereCols.map((p) => `${q(p)} = ?`).join(' AND ')};`;
|
return `DELETE FROM ${target}\nWHERE ${whereCols.map((p) => `${q(p)} = ?`).join(' AND ')};`;
|
||||||
|
|||||||
@@ -540,14 +540,6 @@ export default function QueriesPage() {
|
|||||||
onPageChange={handleExecute}
|
onPageChange={handleExecute}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* -- 安全提示 -- */}
|
|
||||||
<div className="shrink-0 px-4 pb-3">
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 bg-yellow-500/5 border border-yellow-500/15 rounded-lg text-xs text-[var(--dg-text-muted)]">
|
|
||||||
<AlertTriangle className="w-3.5 h-3.5 text-yellow-400 shrink-0" />
|
|
||||||
查询结果中的敏感字段将自动脱敏。MySQL/PG 仅支持 SELECT 查询,Redis/MongoDB 支持只读命令。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* end 编辑器+结果区 */}
|
{/* end 编辑器+结果区 */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user