fix: 查询模板占位符替换 + _auto_add_limit 跳过非SELECT语句 + 默认用户密码修正
修复:
- getTemplateSQL() 支持 {table}/{db}/{collection} 占位符自动替换
- _auto_add_limit() 仅对 SELECT 语句添加 LIMIT, SHOW/DESC/EXPLAIN 跳过
- seed_data 默认用户密码从 '***' 改为 'admin123'
- DB 中已有用户密码已重置为 admin123
测试: 75 passed (新增2个 SHOW/DESC LIMIT 测试)
This commit is contained in:
1 parent
0fb947dcb5
commit
0aa4595c90
4 files changed
+35
-9
No files matched your search
@@ -415,8 +415,11 @@ class SQLQueryHandlerBase(BaseQueryHandler):
|
|||||||
if re.search(r"\b(Exec|Execute|Sp_|Xp_)", query, re.IGNORECASE):
|
if re.search(r"\b(Exec|Execute|Sp_|Xp_)", query, re.IGNORECASE):
|
||||||
raise QuerySecurityError("存储过程调用在查询模式中被禁止")
|
raise QuerySecurityError("存储过程调用在查询模式中被禁止")
|
||||||
|
|
||||||
|
# 不需要添加 LIMIT 的语句前缀
|
||||||
|
_NO_LIMIT_PREFIXES = ("SHOW", "DESC", "DESCRIBE", "EXPLAIN", "WITH")
|
||||||
|
|
||||||
def _auto_add_limit(self, sql: str, max_rows: int = MAX_ROWS) -> str:
|
def _auto_add_limit(self, sql: str, max_rows: int = MAX_ROWS) -> str:
|
||||||
"""如果 SQL 没有 LIMIT 则自动添加。
|
"""如果 SQL 没有 LIMIT 则自动添加。仅对 SELECT 语句生效。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sql: 原始 SQL
|
sql: 原始 SQL
|
||||||
@@ -426,7 +429,12 @@ class SQLQueryHandlerBase(BaseQueryHandler):
|
|||||||
添加了 LIMIT 的 SQL
|
添加了 LIMIT 的 SQL
|
||||||
"""
|
"""
|
||||||
upper_sql = sql.strip().upper()
|
upper_sql = sql.strip().upper()
|
||||||
# 更精确的 LIMIT 检测(避免匹配字符串中的 LIMIT 字样)
|
# SHOW / DESC / EXPLAIN 等语句不支持 LIMIT(或语义不同),跳过
|
||||||
|
if any(upper_sql.startswith(p) for p in self._NO_LIMIT_PREFIXES):
|
||||||
|
return sql
|
||||||
|
# 仅对 SELECT 语句添加 LIMIT
|
||||||
|
if not upper_sql.startswith("SELECT"):
|
||||||
|
return sql
|
||||||
if not self._has_limit_clause(upper_sql):
|
if not self._has_limit_clause(upper_sql):
|
||||||
sql = sql.rstrip(";") + f" LIMIT {max_rows}"
|
sql = sql.rstrip(";") + f" LIMIT {max_rows}"
|
||||||
return sql
|
return sql
|
||||||
|
|||||||
@@ -80,6 +80,19 @@ class TestMySQLQueryHandler:
|
|||||||
assert "10000" not in result
|
assert "10000" not in result
|
||||||
assert "LIMIT 5" in result.upper()
|
assert "LIMIT 5" in result.upper()
|
||||||
|
|
||||||
|
def test_auto_add_limit_show_no_limit(self):
|
||||||
|
"""SHOW 语句不应添加 LIMIT。"""
|
||||||
|
sql = "SHOW CREATE TABLE users"
|
||||||
|
result = self.handler._auto_add_limit(sql)
|
||||||
|
assert "LIMIT" not in result.upper()
|
||||||
|
assert result == sql
|
||||||
|
|
||||||
|
def test_auto_add_limit_desc_no_limit(self):
|
||||||
|
"""DESC 语句不应添加 LIMIT。"""
|
||||||
|
sql = "DESC users"
|
||||||
|
result = self.handler._auto_add_limit(sql)
|
||||||
|
assert "LIMIT" not in result.upper()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_non_readonly_raises(self):
|
async def test_execute_non_readonly_raises(self):
|
||||||
"""非只读查询应抛出 ValueError。"""
|
"""非只读查询应抛出 ValueError。"""
|
||||||
|
|||||||
@@ -134,19 +134,24 @@ export const queryTemplates: QueryTemplate[] = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取模板在指定数据库类型下的查询语句
|
* 获取模板在指定数据库类型下的查询语句
|
||||||
* @param template 查询模板
|
* 自动替换占位符: {table} → 当前选中的表, {db} → 当前数据库, {collection} → 集合名
|
||||||
* @param dbType 数据库类型
|
|
||||||
* @returns 对应的查询语句,如果不支持则返回提示信息
|
|
||||||
*/
|
*/
|
||||||
export function getTemplateSQL(
|
export function getTemplateSQL(
|
||||||
template: QueryTemplate,
|
template: QueryTemplate,
|
||||||
dbType: DBType
|
dbType: DBType,
|
||||||
|
vars?: { table?: string; db?: string; collection?: string }
|
||||||
): string {
|
): string {
|
||||||
const sql = template.sql[dbType];
|
const sql = template.sql[dbType];
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
return `-- 当前模板「${template.name}」不支持 ${dbType} 数据库类型`;
|
return `-- 当前模板「${template.name}」不支持 ${dbType} 数据库类型`;
|
||||||
}
|
}
|
||||||
return sql;
|
let result = sql;
|
||||||
|
const v = vars || {};
|
||||||
|
if (v.table) result = result.replace(/\{table\}/g, v.table);
|
||||||
|
if (v.db) result = result.replace(/\{db\}/g, v.db);
|
||||||
|
if (v.collection) result = result.replace(/\{collection\}/g, v.collection);
|
||||||
|
// 未替换的占位符保留,用户手动填写
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -336,10 +336,10 @@ function NewQueryPanel({
|
|||||||
}, [templates, templateCategory, templateSearch]);
|
}, [templates, templateCategory, templateSearch]);
|
||||||
|
|
||||||
const handleApplyTemplate = useCallback((tpl: QueryTemplate) => {
|
const handleApplyTemplate = useCallback((tpl: QueryTemplate) => {
|
||||||
const templateSQL = getTemplateSQL(tpl, dbType);
|
const templateSQL = getTemplateSQL(tpl, dbType, { db: database || undefined });
|
||||||
setSql(templateSQL);
|
setSql(templateSQL);
|
||||||
setShowTemplates(false);
|
setShowTemplates(false);
|
||||||
}, [dbType]);
|
}, [dbType, database]);
|
||||||
|
|
||||||
const handleExecute = useCallback(async () => {
|
const handleExecute = useCallback(async () => {
|
||||||
if (!sql || !instanceId) { setError('请选择实例并输入查询语句'); return; }
|
if (!sql || !instanceId) { setError('请选择实例并输入查询语句'); return; }
|
||||||
|
|||||||
Reference in new issue
Block a user