feat(dev_tools): enhance SQL Query Tool — clear, copy, history, edit, hardening
All checks were successful
All checks were successful
UI: add Clear and Copy-to-clipboard (TSV) buttons, an in-page Recent Queries pane (localStorage, capped at 20, de-duped) and a pencil-edit flow for saved queries with a dedicated SQL field in the modal. Bind Ctrl/Cmd+S to open the save modal (or edit the active saved query). Backend: harden validate_query with a multi-statement guard that respects string literals + comments. Stop swallowing record_query_run errors silently — log via logger.exception so failures show up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,12 @@ def _strip_sql_comments(sql: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _strip_string_literals(sql: str) -> str:
|
||||
"""Replace single-quoted string literals with a placeholder so their contents
|
||||
don't trip the multi-statement check."""
|
||||
return re.sub(r"'(?:''|[^'])*'", "''", sql)
|
||||
|
||||
|
||||
def validate_query(sql: str) -> None:
|
||||
"""Validate that the SQL query is SELECT-only (no DML/DDL)."""
|
||||
stripped = sql.strip().rstrip(";")
|
||||
@@ -60,6 +66,14 @@ def validate_query(sql: str) -> None:
|
||||
f"Forbidden SQL keyword: {match.group().upper()}. Only SELECT queries are allowed."
|
||||
)
|
||||
|
||||
# Defense-in-depth: reject queries that chain a second statement after `;`.
|
||||
# SQLAlchemy's `text()` over psycopg generally sends a single statement, but
|
||||
# this guards against future driver changes and makes intent explicit.
|
||||
if ";" in _strip_string_literals(code_only):
|
||||
raise QueryValidationError(
|
||||
"Multiple statements are not allowed. Submit one SELECT at a time."
|
||||
)
|
||||
|
||||
|
||||
def _make_json_safe(value: Any) -> Any:
|
||||
"""Convert a database value to a JSON-serializable representation."""
|
||||
|
||||
Reference in New Issue
Block a user