Fix medium/low severity issues: script splitter, EXPLAIN, keyring, imports

execute_script() semicolon splitter (all drivers)
- Add BaseDriver._split_statements() that tracks single/double-quoted
  strings and -- / /* */ comments so semicolons inside procedure bodies
  are not treated as statement boundaries.
- Replace the naive sql.split(';') in all four drivers with this helper.
- MSSQL execute_script() also pre-splits on GO (case-insensitive, own
  line) so scripts pasted from SSMS work correctly.

MSSQL EXPLAIN
- SET SHOWPLAN_TEXT ON/execute/SET SHOWPLAN_TEXT OFF must be separate
  execute() calls; pyodbc rejects multiple statements in one call.
  Hold self._lock for the entire sequence to keep session state atomic.

Keyring (connections.py)
- Log warnings (with traceback) on load/save failures instead of
  silently returning "".
- save_password() now calls delete_password() when password is empty,
  so clearing a saved password actually removes the old keyring entry
  rather than leaving a stale one behind.

SQLite get_tables() O(N) lock acquisitions
- Run all COUNT(*) queries inside the same `with self._cur() as c:`
  block, reducing N+1 lock acquisitions to 1.

Unused import: remove psycopg2.extras from postgres_driver.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:12:40 -04:00
co-authored by Claude Sonnet 4.6
parent 4ccc81955e
commit 04476bae11
6 changed files with 101 additions and 36 deletions
+30 -17
View File
@@ -233,21 +233,23 @@ class MSSQLDriver(BaseDriver):
return [], [], c.rowcount
def execute_script(self, sql: str) -> list:
import re
results = []
stmts = [s.strip() for s in sql.split(';') if s.strip()]
for stmt in stmts:
try:
with self._cur() as c:
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = [tuple(r) for r in c.fetchall()]
results.append((cols, rows, len(rows), ""))
else:
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
batches = re.split(r'^\s*GO\s*$', sql, flags=re.IGNORECASE | re.MULTILINE)
for batch in batches:
for stmt in self._split_statements(batch):
try:
with self._cur() as c:
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = [tuple(r) for r in c.fetchall()]
results.append((cols, rows, len(rows), ""))
else:
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
return results
# ── Table data CRUD ───────────────────────────────────────────────────────
@@ -305,9 +307,20 @@ class MSSQLDriver(BaseDriver):
# ── Server tools ──────────────────────────────────────────────────────────
def explain_query(self, sql: str) -> tuple:
with self._cur() as c:
c.execute(f"SET SHOWPLAN_TEXT ON; {sql}; SET SHOWPLAN_TEXT OFF")
return ["Plan"], c.fetchall()
with self._lock:
cur = self._connection.cursor()
try:
cur.execute("SET SHOWPLAN_TEXT ON")
cur.execute(sql)
cols = [d[0] for d in cur.description] if cur.description else ["Plan"]
rows = cur.fetchall()
cur.execute("SET SHOWPLAN_TEXT OFF")
finally:
try:
cur.close()
except Exception:
pass
return cols, rows
def get_process_list(self) -> tuple:
with self._cur() as c: