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
+46
View File
@@ -197,6 +197,52 @@ class BaseDriver(ABC):
"""Rename a column via ALTER TABLE."""
pass
@staticmethod
def _split_statements(sql: str) -> list:
"""Split SQL text on unquoted semicolons, preserving quoted strings and comments."""
stmts, buf = [], []
i, n = 0, len(sql)
while i < n:
ch = sql[i]
if ch == '-' and i + 1 < n and sql[i + 1] == '-':
end = sql.find('\n', i)
end = end + 1 if end != -1 else n
buf.append(sql[i:end])
i = end
elif ch == '/' and i + 1 < n and sql[i + 1] == '*':
end = sql.find('*/', i + 2)
end = end + 2 if end != -1 else n
buf.append(sql[i:end])
i = end
elif ch in ("'", '"'):
q, j = ch, i + 1
while j < n:
if sql[j] == '\\':
j += 2
elif sql[j] == q:
j += 1
if j < n and sql[j] == q:
j += 1
continue
break
else:
j += 1
buf.append(sql[i:j])
i = j
elif ch == ';':
stmt = ''.join(buf).strip()
if stmt:
stmts.append(stmt)
buf = []
i += 1
else:
buf.append(ch)
i += 1
stmt = ''.join(buf).strip()
if stmt:
stmts.append(stmt)
return stmts
@property
def is_connected(self) -> bool:
return self._connection is not None