mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
✨ feat(postgres): PostgreSQL 支持不带 schema 前缀的表名补全与执行
- 后端优化:连接成功后自动查询所有用户 schema 并将 search_path 写入 DSN 重建连接池 - 连接池修复:SET search_path 仅对单个连接生效,改为 DSN 级别配置使所有连接生效 - 表名补全:前端智能匹配 schema.table 中的纯表名部分,输入表名即可触发补全 - 同名表处理:跨 schema 存在同名表时补全自动显示 schema.table 格式以区分 - 列补全增强:FROM/JOIN 引用纯表名时关联列提示和别名列提示均可正确匹配
This commit is contained in:
@@ -1 +1 @@
|
||||
20168ff7047e0ecea00acb73f413f7db
|
||||
f697e821b4acd5cf614d63d46453e8a4
|
||||
@@ -716,11 +716,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
// Prefer preloaded MySQL all-columns cache
|
||||
let cols: { name: string, type?: string, tableName?: string, dbName?: string }[];
|
||||
if (sharedAllColumnsData.length > 0) {
|
||||
const tiTableLower = (tableInfo.tableName || '').toLowerCase();
|
||||
cols = sharedAllColumnsData
|
||||
.filter(c =>
|
||||
(c.dbName || '').toLowerCase() === (tableInfo.dbName || '').toLowerCase() &&
|
||||
(c.tableName || '').toLowerCase() === (tableInfo.tableName || '').toLowerCase()
|
||||
)
|
||||
.filter(c => {
|
||||
if ((c.dbName || '').toLowerCase() !== (tableInfo.dbName || '').toLowerCase()) return false;
|
||||
const cTableLower = (c.tableName || '').toLowerCase();
|
||||
if (cTableLower === tiTableLower) return true;
|
||||
// schema.table 格式匹配纯表名
|
||||
const parsed = splitSchemaAndTable(c.tableName || '');
|
||||
return (parsed.table || '').toLowerCase() === tiTableLower;
|
||||
})
|
||||
.map(c => ({ name: c.name, type: c.type, tableName: c.tableName, dbName: c.dbName }));
|
||||
} else {
|
||||
const dbCols = await getColumnsByDB(tableInfo.tableName);
|
||||
@@ -773,7 +778,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
.filter(c => {
|
||||
const fullIdent = `${c.dbName}.${c.tableName}`.toLowerCase();
|
||||
const shortIdent = (c.tableName || '').toLowerCase();
|
||||
return (foundTables.has(fullIdent) || foundTables.has(shortIdent)) && startsWithPrefix(c.name || '');
|
||||
// 对 schema.table 格式,也用纯表名部分匹配(如 public.users → users)
|
||||
const parsed = splitSchemaAndTable(c.tableName || '');
|
||||
const pureIdent = (parsed.table || '').toLowerCase();
|
||||
return (foundTables.has(fullIdent) || foundTables.has(shortIdent) || (pureIdent && foundTables.has(pureIdent))) && startsWithPrefix(c.name || '');
|
||||
})
|
||||
.map(c => {
|
||||
// 当前库的表字段优先级更高
|
||||
@@ -788,24 +796,61 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
};
|
||||
});
|
||||
|
||||
// 表提示:当前库显示表名,其他库显示 db.table 格式
|
||||
// 表提示:当前库智能处理 schema.table 格式
|
||||
// 1. 构建纯表名到 schema 列表的映射,检测同名表
|
||||
const currentDbTables = sharedTablesData.filter(t =>
|
||||
(t.dbName || '').toLowerCase() === currentDatabase.toLowerCase()
|
||||
);
|
||||
const tableNameToSchemas = new Map<string, string[]>();
|
||||
for (const t of currentDbTables) {
|
||||
const parsed = splitSchemaAndTable(t.tableName || '');
|
||||
const pureTable = (parsed.table || t.tableName || '').toLowerCase();
|
||||
const schemas = tableNameToSchemas.get(pureTable) || [];
|
||||
schemas.push(parsed.schema || '');
|
||||
tableNameToSchemas.set(pureTable, schemas);
|
||||
}
|
||||
|
||||
const tableSuggestions = sharedTablesData
|
||||
.filter(t => {
|
||||
const isCurrentDb = (t.dbName || '').toLowerCase() === currentDatabase.toLowerCase();
|
||||
const label = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`;
|
||||
return startsWithPrefix(label || '');
|
||||
if (!isCurrentDb) {
|
||||
// 跨库:用 db.table 格式匹配
|
||||
return startsWithPrefix(`${t.dbName}.${t.tableName}`);
|
||||
}
|
||||
// 当前库:同时用完整名和纯表名匹配
|
||||
const parsed = splitSchemaAndTable(t.tableName || '');
|
||||
const pureTable = parsed.table || t.tableName || '';
|
||||
return startsWithPrefix(t.tableName || '') || startsWithPrefix(pureTable);
|
||||
})
|
||||
.map(t => {
|
||||
const isCurrentDb = (t.dbName || '').toLowerCase() === currentDatabase.toLowerCase();
|
||||
const label = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`;
|
||||
const insertText = isCurrentDb ? t.tableName : `${t.dbName}.${t.tableName}`;
|
||||
if (!isCurrentDb) {
|
||||
const label = `${t.dbName}.${t.tableName}`;
|
||||
return {
|
||||
label,
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: label,
|
||||
detail: `Table (${t.dbName})`,
|
||||
range,
|
||||
sortText: sortGroups.tableOther + t.tableName,
|
||||
};
|
||||
}
|
||||
// 当前库:检查是否有跨 schema 同名表
|
||||
const parsed = splitSchemaAndTable(t.tableName || '');
|
||||
const pureTable = parsed.table || t.tableName || '';
|
||||
const schemas = tableNameToSchemas.get(pureTable.toLowerCase()) || [];
|
||||
const hasDuplicate = schemas.length > 1;
|
||||
// 同名表存在于多个 schema → 显示 schema.table;否则只显示纯表名
|
||||
const label = hasDuplicate ? t.tableName : pureTable;
|
||||
const insertText = hasDuplicate ? t.tableName : pureTable;
|
||||
const schemaInfo = parsed.schema ? ` (${parsed.schema})` : '';
|
||||
return {
|
||||
label,
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText,
|
||||
detail: `Table (${t.dbName})`,
|
||||
detail: `Table${schemaInfo}`,
|
||||
range,
|
||||
sortText: isCurrentDb ? sortGroups.tableCurrent + t.tableName : sortGroups.tableOther + t.tableName,
|
||||
sortText: sortGroups.tableCurrent + pureTable,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -166,6 +166,9 @@ func (p *PostgresDB) Connect(config connection.ConnectionConfig) error {
|
||||
logger.Infof("PostgreSQL 自动选择连接数据库:%s", dbName)
|
||||
}
|
||||
|
||||
// 设置 search_path,使所有用户 schema 下的表可以不带 schema 前缀访问
|
||||
p.ensureSearchPath(dsn)
|
||||
|
||||
cleanupOnFailure = false
|
||||
return nil
|
||||
}
|
||||
@@ -611,6 +614,101 @@ ORDER BY table_schema, table_name, ordinal_position`
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
// ensureSearchPath 查询当前数据库中所有用户 schema,通过重建连接池将 search_path 写入 DSN。
|
||||
// 仅使用 SET search_path 只对连接池中的单个连接生效,后续查询可能拿到未设置的连接。
|
||||
// 将 search_path 写入 DSN (lib/pq 支持任意 PostgreSQL runtime parameter),
|
||||
// 使连接池中每个连接建立时自动携带 search_path,与金仓行为一致。
|
||||
func (p *PostgresDB) ensureSearchPath(baseDSN string) {
|
||||
if p.conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rawSchemas := p.queryUserSchemas()
|
||||
if len(rawSchemas) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 构建 search_path SQL 片段(带双引号转义),用于 SET 兜底
|
||||
searchPathSQL, normalizedSchemas := buildKingbaseSearchPathCommon(rawSchemas)
|
||||
if strings.TrimSpace(searchPathSQL) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 策略 1:将 search_path 写入 DSN,重建连接池
|
||||
// lib/pq 支持在 URL 查参数中设置任意 PostgreSQL runtime parameter,
|
||||
// 如 ?search_path=ce,public,每个新连接建立时会自动 SET search_path。
|
||||
searchPathDSNVal := strings.Join(normalizedSchemas, ",")
|
||||
u, parseErr := url.Parse(baseDSN)
|
||||
if parseErr == nil {
|
||||
q := u.Query()
|
||||
q.Set("search_path", searchPathDSNVal)
|
||||
u.RawQuery = q.Encode()
|
||||
newDSN := u.String()
|
||||
|
||||
newDB, err := sql.Open("postgres", newDSN)
|
||||
if err == nil {
|
||||
newDB.SetConnMaxLifetime(5 * time.Minute)
|
||||
oldConn := p.conn
|
||||
p.conn = newDB
|
||||
if err := p.Ping(); err == nil {
|
||||
_ = oldConn.Close()
|
||||
logger.Infof("PostgreSQL 已通过 DSN 配置 search_path:%s", searchPathDSNVal)
|
||||
return
|
||||
}
|
||||
// DSN 方式失败,回滚
|
||||
_ = newDB.Close()
|
||||
p.conn = oldConn
|
||||
logger.Warnf("PostgreSQL DSN search_path 验证失败,回退至 SET 方式")
|
||||
}
|
||||
}
|
||||
|
||||
// 策略 2 兜底:通过 SET search_path 设置(仅影响单个连接,但聊胜于无)
|
||||
timeout := p.pingTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := p.conn.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s", searchPathSQL)); err != nil {
|
||||
logger.Warnf("PostgreSQL 设置 search_path 失败:%v", err)
|
||||
return
|
||||
}
|
||||
logger.Infof("PostgreSQL 已通过 SET 设置 search_path:%s", searchPathSQL)
|
||||
}
|
||||
|
||||
// queryUserSchemas 查询当前数据库中所有用户 schema。
|
||||
func (p *PostgresDB) queryUserSchemas() []string {
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := `SELECT nspname FROM pg_namespace
|
||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND nspname NOT LIKE 'pg_%'
|
||||
ORDER BY nspname`
|
||||
|
||||
rows, err := p.conn.Query(query)
|
||||
if err != nil {
|
||||
logger.Warnf("PostgreSQL 查询用户 schema 失败:%v", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var schemas []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
schemas = append(schemas, name)
|
||||
}
|
||||
}
|
||||
return schemas
|
||||
}
|
||||
|
||||
func (p *PostgresDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
|
||||
if p.conn == nil {
|
||||
return fmt.Errorf("连接未打开")
|
||||
|
||||
Reference in New Issue
Block a user