dbqm 2.3.0__tar.gz → 2.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {dbqm-2.3.0 → dbqm-2.3.2}/PKG-INFO +2 -1
  2. dbqm-2.3.2/dbqm/_version.py +1 -0
  3. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/query.py +19 -6
  4. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/schema.py +1 -1
  5. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/envelope.py +2 -2
  6. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/params.py +17 -2
  7. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/render.py +2 -1
  8. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/audit.py +2 -2
  9. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/db_manager.py +5 -6
  10. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_extractor.py +5 -5
  11. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_mysql.py +3 -1
  12. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/exporter.py +5 -6
  13. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/group_engine.py +3 -3
  14. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/html_report.py +2 -2
  15. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/oracle_client_installer.py +1 -1
  16. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/paths.py +1 -1
  17. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/query_engine.py +31 -33
  18. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/read_only.py +9 -2
  19. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/template_engine.py +1 -1
  20. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/main.py +1 -1
  21. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/connection.py +3 -3
  22. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/settings.py +3 -2
  23. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/template.py +3 -3
  24. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/column_maps.py +1 -1
  25. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/export_picker.py +2 -2
  26. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/param_input.py +1 -1
  27. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/adhoc.py +6 -7
  28. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/browser.py +1 -1
  29. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/config_port.py +2 -1
  30. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/connections.py +1 -1
  31. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/exec_routine.py +1 -1
  32. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/utils.py +2 -2
  33. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/result_table.py +1 -1
  34. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/sql_viewer.py +3 -1
  35. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/PKG-INFO +2 -1
  36. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/requires.txt +1 -0
  37. dbqm-2.3.2/pyproject.toml +150 -0
  38. {dbqm-2.3.0 → dbqm-2.3.2}/tests/test_cli_tema.py +1 -0
  39. dbqm-2.3.0/dbqm/_version.py +0 -1
  40. dbqm-2.3.0/pyproject.toml +0 -61
  41. {dbqm-2.3.0 → dbqm-2.3.2}/PYPI.md +0 -0
  42. {dbqm-2.3.0 → dbqm-2.3.2}/README.md +0 -0
  43. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/__init__.py +0 -0
  44. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/__main__.py +0 -0
  45. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/__init__.py +0 -0
  46. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/__init__.py +0 -0
  47. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/config_bundle.py +0 -0
  48. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/connection.py +0 -0
  49. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/inspect.py +0 -0
  50. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/deps.py +0 -0
  51. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/errors.py +0 -0
  52. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/__init__.py +0 -0
  53. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/config_portability.py +0 -0
  54. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/connection_builder.py +0 -0
  55. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/crypto.py +0 -0
  56. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_pg.py +0 -0
  57. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/history.py +0 -0
  58. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/object_browser.py +0 -0
  59. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/package_editor.py +0 -0
  60. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/table_browser.py +0 -0
  61. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/design/__init__.py +0 -0
  62. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/design/tokens.py +0 -0
  63. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/__init__.py +0 -0
  64. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/group.py +0 -0
  65. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/query.py +0 -0
  66. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/__init__.py +0 -0
  67. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/app.py +0 -0
  68. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/__init__.py +0 -0
  69. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/confirm.py +0 -0
  70. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/error.py +0 -0
  71. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/export_dir_setup.py +0 -0
  72. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/help.py +0 -0
  73. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/oracle_client_dir.py +0 -0
  74. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/text_input.py +0 -0
  75. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/__init__.py +0 -0
  76. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_exec.py +0 -0
  77. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_manage.py +0 -0
  78. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_run.py +0 -0
  79. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/history.py +0 -0
  80. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/oracle_clients.py +0 -0
  81. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/package_editor.py +0 -0
  82. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/query_exec.py +0 -0
  83. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/query_manage.py +0 -0
  84. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/settings.py +0 -0
  85. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/template_manage.py +0 -0
  86. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/tools.py +0 -0
  87. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/theme.py +0 -0
  88. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/__init__.py +0 -0
  89. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/action_bar.py +0 -0
  90. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/dialog.py +0 -0
  91. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/empty_state.py +0 -0
  92. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/group_result.py +0 -0
  93. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/hierarchical_list.py +0 -0
  94. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/panel.py +0 -0
  95. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/progress.py +0 -0
  96. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/query_list.py +0 -0
  97. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/skeleton.py +0 -0
  98. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/status_bar.py +0 -0
  99. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/templates_sidebar.py +0 -0
  100. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/verdict.py +0 -0
  101. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/SOURCES.txt +0 -0
  102. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/dependency_links.txt +0 -0
  103. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/entry_points.txt +0 -0
  104. {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/top_level.txt +0 -0
  105. {dbqm-2.3.0 → dbqm-2.3.2}/setup.cfg +0 -0
  106. {dbqm-2.3.0 → dbqm-2.3.2}/tests/test_cli.py +0 -0
  107. {dbqm-2.3.0 → dbqm-2.3.2}/tests/test_cli_markup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbqm
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Multi-database interactive query tool with fullscreen TUI
5
5
  Author: Silvio Chagas
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ Requires-Dist: pymssql>=2.2; sys_platform != "win32" or platform_machine != "ARM
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: pytest>=7.0; extra == "dev"
25
25
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
26
+ Requires-Dist: mypy>=1.11; extra == "dev"
26
27
  Provides-Extra: oracle
27
28
  Requires-Dist: oracledb>=2.0; extra == "oracle"
28
29
  Provides-Extra: postgres
@@ -0,0 +1 @@
1
+ __version__ = "2.3.2"
@@ -23,8 +23,10 @@ from dbqm.cli.render import console
23
23
  # by text and remaps it here; anything else really is `sql_error`.
24
24
  _USAGE_SQL_MESSAGES = (
25
25
  "Apenas comandos SELECT sao permitidos.",
26
- "Tipo de SQL nao suportado. Use SELECT, INSERT, UPDATE, DELETE, DDL "
27
- "(CREATE/ALTER/DROP...) ou EXPLAIN PLAN.",
26
+ (
27
+ "Tipo de SQL nao suportado. Use SELECT, INSERT, UPDATE, DELETE, DDL "
28
+ "(CREATE/ALTER/DROP...) ou EXPLAIN PLAN."
29
+ ),
28
30
  "Passe apenas a query (sem EXPLAIN PLAN FOR) ao usar --explain.",
29
31
  )
30
32
 
@@ -319,12 +321,23 @@ def cmd_sql(args: argparse.Namespace) -> None:
319
321
  _fail_or_print(args, "sql", "usage", "DML requer --commit para confirmar a operacao.")
320
322
 
321
323
  try:
322
- result = deps.execute_adhoc(sql, conn, param_values, auto_commit=args.commit)
324
+ outcome = deps.execute_adhoc(sql, conn, param_values, auto_commit=args.commit)
323
325
  except deps.ReadOnlyViolation as e:
324
326
  _fail_or_print(args, "sql", "read_only", str(e))
325
327
 
328
+ # `execute_adhoc` only returns the `(result, connection)` tuple for a DML
329
+ # statement left uncommitted (`auto_commit=False`); the --commit guard
330
+ # above already exits before this call whenever `sql_type` is DML and
331
+ # `--commit` was not given, and no other `sql_type` ever produces that
332
+ # tuple. So this call always yields a plain `AdhocResult`.
333
+ if isinstance(outcome, tuple): # pragma: no cover - unreachable, see above
334
+ raise RuntimeError(
335
+ "execute_adhoc returned a manual-commit tuple despite auto_commit=True"
336
+ )
337
+ result = outcome
338
+
326
339
  # For non-SELECT results (always AdhocResult with auto_commit=True at this point)
327
- if not isinstance(result, tuple) and result.sql_type in ("INSERT", "UPDATE", "DELETE"):
340
+ if result.sql_type in ("INSERT", "UPDATE", "DELETE"):
328
341
  if not result.success:
329
342
  _fail_or_print(args, "sql", _sql_error_code(result.error, result.error_kind),
330
343
  result.error or "Erro ao executar SQL.")
@@ -335,7 +348,7 @@ def cmd_sql(args: argparse.Namespace) -> None:
335
348
  return
336
349
 
337
350
  # DDL results
338
- if not isinstance(result, tuple) and result.sql_type == "DDL":
351
+ if result.sql_type == "DDL":
339
352
  if not result.success:
340
353
  code = _sql_error_code(result.error, result.error_kind)
341
354
  if args.format == "json":
@@ -350,7 +363,7 @@ def cmd_sql(args: argparse.Namespace) -> None:
350
363
  return
351
364
 
352
365
  # PL/SQL anonymous block results
353
- if not isinstance(result, tuple) and result.sql_type == "PLSQL":
366
+ if result.sql_type == "PLSQL":
354
367
  if not result.success:
355
368
  _fail_or_print(args, "sql", _sql_error_code(result.error, result.error_kind),
356
369
  result.error or "Erro ao executar bloco.")
@@ -209,7 +209,7 @@ def cmd_rows(args: argparse.Namespace) -> None:
209
209
  if args.table.upper() not in tabelas | vistas:
210
210
  raise deps.ObjectNotFound(
211
211
  f"Tabela '{args.table}' nao encontrada em {conn.name}."
212
- )
212
+ ) from original
213
213
  raise
214
214
 
215
215
  resultado = _with_open_connection(args, "rows", conn, acao)
@@ -17,12 +17,12 @@ from __future__ import annotations
17
17
 
18
18
  import json
19
19
  import sys
20
- from typing import Any, NoReturn
20
+ from typing import Any, NoReturn, TextIO
21
21
 
22
22
  from dbqm.cli.errors import exit_for
23
23
 
24
24
 
25
- def _write(stream, payload: dict) -> None:
25
+ def _write(stream: TextIO, payload: dict[str, Any]) -> None:
26
26
  json.dump(payload, stream, indent=2, ensure_ascii=False, default=str)
27
27
  stream.write("\n")
28
28
 
@@ -5,6 +5,7 @@ import argparse
5
5
  import getpass
6
6
  import os
7
7
  import sys
8
+ from typing import Literal, overload
8
9
 
9
10
  from rich.markup import escape
10
11
 
@@ -14,7 +15,7 @@ from dbqm.cli.render import console
14
15
 
15
16
 
16
17
  def _parse_params(param_list: list[str] | None, args: argparse.Namespace | None = None,
17
- command: str = "params") -> dict:
18
+ command: str = "params") -> dict[str, str]:
18
19
  """Parse key=value parameter pairs from CLI arguments.
19
20
 
20
21
  `args`/`command` let a malformed `-p` speak the envelope under `-f json`
@@ -77,6 +78,16 @@ def _add_connection_fields(parser: argparse.ArgumentParser) -> None:
77
78
  help="Formato de saida")
78
79
 
79
80
 
81
+ @overload
82
+ def resolve_password(
83
+ args: argparse.Namespace, env_var: str, prompt: str, *, required: Literal[True],
84
+ use_env: bool = True, command: str = "password",
85
+ ) -> str: ...
86
+ @overload
87
+ def resolve_password(
88
+ args: argparse.Namespace, env_var: str, prompt: str, *, required: bool,
89
+ use_env: bool = True, command: str = "password",
90
+ ) -> str | None: ...
80
91
  def resolve_password(
81
92
  args: argparse.Namespace, env_var: str, prompt: str, *, required: bool,
82
93
  use_env: bool = True, command: str = "password",
@@ -110,7 +121,11 @@ def resolve_password(
110
121
  `table`'s unchanged prose-and-`sys.exit` branch.
111
122
  """
112
123
  from_stdin = getattr(args, "password_stdin", False)
113
- direct = getattr(args, "password", None)
124
+ # `args.password`, where the parser defines it, is a plain string
125
+ # argument (argparse's `--password`). `argparse.Namespace` is untyped
126
+ # so `getattr` returns `Any`; the annotation states what the value
127
+ # actually is instead of letting that `Any` leak into the return type.
128
+ direct: str | None = getattr(args, "password", None)
114
129
  fmt = getattr(args, "format", "table")
115
130
 
116
131
  if from_stdin and direct:
@@ -7,6 +7,7 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
  from rich.theme import Theme as _RichTheme
9
9
 
10
+ from dbqm.core.group_engine import ComparisonResult
10
11
  from dbqm.design.tokens import DARK_TOKENS
11
12
 
12
13
 
@@ -68,7 +69,7 @@ def _print_query_result(result: Any, output_format: str = "table") -> None:
68
69
  console.print(f"[dim]{result.row_count} registros em {result.elapsed:.2f}s[/dim]")
69
70
 
70
71
 
71
- def _colored_comparison_lines(comparisons: list) -> list[str]:
72
+ def _colored_comparison_lines(comparisons: list[ComparisonResult]) -> list[str]:
72
73
  """Linhas de resumo do grupo, coloridas pelo eixo de veredito.
73
74
 
74
75
  Reconstroi o texto a partir de `ComparisonResult` (contagens), em vez de
@@ -16,7 +16,7 @@ def log_execution(
16
16
  action: str,
17
17
  name: str,
18
18
  connection: str = "",
19
- params: dict | None = None,
19
+ params: dict[str, str] | None = None,
20
20
  row_count: int = 0,
21
21
  success: bool = True,
22
22
  error: str = "",
@@ -39,7 +39,7 @@ def log_execution(
39
39
 
40
40
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
41
41
  is_new = not AUDIT_FILE.exists()
42
- with open(AUDIT_FILE, "a", encoding="utf-8") as f:
42
+ with AUDIT_FILE.open("a", encoding="utf-8") as f:
43
43
  f.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
44
44
  if is_new:
45
45
  try:
@@ -103,7 +103,7 @@ def _python_is_64bit() -> bool:
103
103
  def _pe_machine(dll: Path) -> int | None:
104
104
  """Read the COFF machine field of a PE binary. None when unreadable."""
105
105
  try:
106
- with open(dll, "rb") as f:
106
+ with dll.open("rb") as f:
107
107
  f.seek(0x3C)
108
108
  pe_offset = int.from_bytes(f.read(4), "little")
109
109
  f.seek(pe_offset + 4)
@@ -450,14 +450,13 @@ def get_connection(conn: Connection) -> Any:
450
450
  """Get a database connection based on connection type."""
451
451
  if conn.db_type == "oracle":
452
452
  return get_oracle_connection(conn)
453
- elif conn.db_type == "sqlserver":
453
+ if conn.db_type == "sqlserver":
454
454
  return get_sqlserver_connection(conn)
455
- elif conn.db_type == "postgresql":
455
+ if conn.db_type == "postgresql":
456
456
  return get_postgresql_connection(conn)
457
- elif conn.db_type == "mysql":
457
+ if conn.db_type == "mysql":
458
458
  return get_mysql_connection(conn)
459
- else:
460
- raise ValueError(f"Tipo de banco desconhecido: {conn.db_type}")
459
+ raise ValueError(f"Tipo de banco desconhecido: {conn.db_type}")
461
460
 
462
461
 
463
462
  def fetch_table_columns(conn: Connection, table: str) -> list[str]:
@@ -245,9 +245,9 @@ def _build_constraint_ddl_fallback(
245
245
 
246
246
  if c_type == "P":
247
247
  return f"ALTER TABLE {owner}.{table_name} ADD CONSTRAINT {c_name} PRIMARY KEY ({col_list});"
248
- elif c_type == "U":
248
+ if c_type == "U":
249
249
  return f"ALTER TABLE {owner}.{table_name} ADD CONSTRAINT {c_name} UNIQUE ({col_list});"
250
- elif c_type == "R":
250
+ if c_type == "R":
251
251
  ref = ""
252
252
  if r_owner and r_cname:
253
253
  ref_cols = _query_all(cursor, """
@@ -260,7 +260,7 @@ def _build_constraint_ddl_fallback(
260
260
  ref_col_list = ", ".join(c[1] for c in ref_cols)
261
261
  ref = f" REFERENCES {r_owner}.{ref_table} ({ref_col_list})"
262
262
  return f"ALTER TABLE {owner}.{table_name} ADD CONSTRAINT {c_name} FOREIGN KEY ({col_list}){ref};"
263
- elif c_type == "C":
263
+ if c_type == "C":
264
264
  cond = search_cond.strip() if search_cond else ""
265
265
  return f"ALTER TABLE {owner}.{table_name} ADD CONSTRAINT {c_name} CHECK ({cond});"
266
266
  return f"-- Constraint {c_name} type={c_type}"
@@ -399,7 +399,7 @@ def _parse_routines(source: str) -> dict[str, dict]:
399
399
  if not matches:
400
400
  return routines
401
401
 
402
- for i, m in enumerate(matches):
402
+ for _i, m in enumerate(matches):
403
403
  rtype = m.group(1).upper()
404
404
  rname = m.group(2)
405
405
  start = m.start()
@@ -447,7 +447,7 @@ def _find_routine_end(source: str, offset: int, routine_name: str) -> int:
447
447
 
448
448
  for tok in token_pattern.finditer(source, pos):
449
449
  g = tok.group(0).upper()
450
- if g.startswith("'") or g.startswith("--") or g.startswith("/*"):
450
+ if g.startswith(("'", "--", "/*")):
451
451
  continue
452
452
  if g in ("BEGIN", "CASE"):
453
453
  depth += 1
@@ -1,10 +1,12 @@
1
1
  """MySQL DDL extraction via SHOW CREATE statements."""
2
2
  from __future__ import annotations
3
3
 
4
+ from typing import Any
5
+
4
6
  from dbqm.core.ddl_extractor import ExtractionResult, ExtractedObject
5
7
 
6
8
 
7
- def extract_mysql_ddl(db, object_name: str, result: ExtractionResult, on_progress=None):
9
+ def extract_mysql_ddl(db: Any, object_name: str, result: ExtractionResult, on_progress: Any = None) -> None:
8
10
  """Extract DDL for a MySQL object (table, view, procedure, function)."""
9
11
  cursor = db.cursor()
10
12
  name = object_name.strip()
@@ -37,8 +37,7 @@ _MIN_FILENAME_BODY = 8
37
37
  def _sanitize(name: str) -> str:
38
38
  """Sanitize a name for safe use in filenames."""
39
39
  s = name.replace(" ", "_").replace("/", "_").replace("\\", "_")
40
- s = re.sub(r'[<>:"|?*]', "_", s)
41
- return s
40
+ return re.sub(r'[<>:"|?*]', "_", s)
42
41
 
43
42
 
44
43
  def _normalize_label(name: str) -> str:
@@ -242,7 +241,7 @@ def export_query_csv(result: QueryResult, table: str = "", params: dict | None =
242
241
  """Export a single query result to CSV. Returns the file path."""
243
242
  label = table or result.query_name
244
243
  filepath = _build_filepath("consultas", label, result.connection_name, params, "csv")
245
- with open(filepath, "w", newline="", encoding="utf-8") as f:
244
+ with filepath.open("w", newline="", encoding="utf-8") as f:
246
245
  writer = csv.writer(f)
247
246
  writer.writerow(result.columns)
248
247
  writer.writerows(result.rows)
@@ -259,7 +258,7 @@ def export_query_json(result: QueryResult, table: str = "", params: dict | None
259
258
  "columns": result.columns,
260
259
  "row_count": result.row_count,
261
260
  "elapsed": round(result.elapsed, 3),
262
- "rows": [dict(zip(result.columns, row)) for row in result.rows],
261
+ "rows": [dict(zip(result.columns, row, strict=True)) for row in result.rows],
263
262
  }
264
263
  filepath.write_text(json.dumps(data, indent=2, ensure_ascii=False, default=str), encoding="utf-8")
265
264
  return str(filepath)
@@ -373,7 +372,7 @@ def export_group_csv(group_result: GroupResult, params: dict | None = None) -> s
373
372
 
374
373
  query_names, compare_columns, all_keys, lookup = _build_pivoted_data(group_result)
375
374
 
376
- with open(filepath, "w", newline="", encoding="utf-8") as f:
375
+ with filepath.open("w", newline="", encoding="utf-8") as f:
377
376
  if params:
378
377
  for k, v in params.items():
379
378
  f.write(f"# Parametro: {k} = {v}\n")
@@ -539,7 +538,7 @@ def export_group_flat_csv(group_result: GroupResult, params: dict | None = None)
539
538
 
540
539
  query_names = list(group_result.query_results.keys())
541
540
 
542
- with open(filepath, "w", newline="", encoding="utf-8") as f:
541
+ with filepath.open("w", newline="", encoding="utf-8") as f:
543
542
  if params:
544
543
  for k, v in params.items():
545
544
  f.write(f"# Parametro: {k} = {v}\n")
@@ -92,7 +92,7 @@ def run_comparison(
92
92
  continue
93
93
  for row in result.rows:
94
94
  key_val = row[key_idx]
95
- row_dict = dict(zip(result.columns, row))
95
+ row_dict = dict(zip(result.columns, row, strict=True))
96
96
  indexed[qname][key_val] = row_dict
97
97
 
98
98
  # Collect all unique keys
@@ -138,8 +138,8 @@ def run_comparison(
138
138
  status = "ABSENT"
139
139
  absent_count += 1
140
140
  else:
141
- unique_normalized = set(v for v in values.values() if v is not None)
142
- unique_raw = set(str(v) for v in raw_values.values() if v is not None)
141
+ unique_normalized = {v for v in values.values() if v is not None}
142
+ unique_raw = {str(v) for v in raw_values.values() if v is not None}
143
143
  if len(unique_normalized) <= 1:
144
144
  if len(unique_raw) <= 1:
145
145
  status = "OK"
@@ -29,7 +29,7 @@ def _light_theme_block(tokens: dict[str, str]) -> str:
29
29
  return f"@media (prefers-color-scheme: light) {{\n :root {{\n{linhas}\n }}\n}}"
30
30
 
31
31
 
32
- def export_group_html(group_result: GroupResult, params: dict | None = None) -> str:
32
+ def export_group_html(group_result: GroupResult, params: dict[str, str] | None = None) -> str:
33
33
  """Export group comparison as a standalone HTML file. Returns file path."""
34
34
  query_names = list(group_result.query_results.keys())
35
35
  filepath = _build_filepath("grupos", group_result.group_name, params=params, ext="html")
@@ -47,7 +47,7 @@ def _status_label(status: str) -> str:
47
47
  return {"OK": "OK", "OK*": "OK*", "DIFF": "DIFERE", "ABSENT": "AUSENTE"}.get(status, status)
48
48
 
49
49
 
50
- def _build_html(group_result: GroupResult, query_names: list[str], params: dict | None) -> str:
50
+ def _build_html(group_result: GroupResult, query_names: list[str], params: dict[str, str] | None) -> str:
51
51
  now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
52
52
  overall = "CONSISTENTE" if group_result.all_match else "DIVERGENTE"
53
53
  overall_class = "ok" if group_result.all_match else "diff"
@@ -243,7 +243,7 @@ def _download(url: str, dest: Path, progress: ProgressCallback | None) -> None:
243
243
  total = resp.length # may be None
244
244
  chunk = 1 << 16
245
245
  done = 0
246
- with open(dest, "wb") as out:
246
+ with dest.open("wb") as out:
247
247
  while True:
248
248
  buf = resp.read(chunk)
249
249
  if not buf:
@@ -24,7 +24,7 @@ SETTINGS_FILE = CONFIG_DIR / "settings.json"
24
24
  TEMPLATES_DIR = DBQM_HOME / "templates"
25
25
  TEMPLATES_FILE = TEMPLATES_DIR / "templates.json"
26
26
 
27
- def ensure_dirs():
27
+ def ensure_dirs() -> None:
28
28
  """Create data directories if they don't exist."""
29
29
  for d in [CONFIG_DIR, EXPORTS_DIR, HISTORY_DIR, TEMPLATES_DIR]:
30
30
  d.mkdir(parents=True, exist_ok=True)
@@ -480,7 +480,7 @@ def execute_adhoc(sql: str, conn: Connection, param_values: dict, auto_commit: b
480
480
  elapsed=elapsed,
481
481
  output_lines=output_lines,
482
482
  )
483
- elif sql_type == "DDL":
483
+ if sql_type == "DDL":
484
484
  cursor.close()
485
485
  # For DDL on Oracle, check compilation errors if applicable
486
486
  compilation_errors = _fetch_ddl_errors(db, sql, conn.db_type)
@@ -496,7 +496,7 @@ def execute_adhoc(sql: str, conn: Connection, param_values: dict, auto_commit: b
496
496
  error=compilation_errors,
497
497
  success=not compilation_errors,
498
498
  )
499
- elif sql_type == "PLSQL":
499
+ if sql_type == "PLSQL":
500
500
  output_lines = _read_dbms_output(cursor) if capture_dbms_output else []
501
501
  columns: list[str] = []
502
502
  rows: list[list[Any]] = []
@@ -523,7 +523,7 @@ def execute_adhoc(sql: str, conn: Connection, param_values: dict, auto_commit: b
523
523
  success=True,
524
524
  output_lines=output_lines,
525
525
  )
526
- elif sql_type == "EXPLAIN":
526
+ if sql_type == "EXPLAIN":
527
527
  # Oracle EXPLAIN PLAN inserts into PLAN_TABLE — no result set.
528
528
  # PostgreSQL/MySQL EXPLAIN returns the plan as rows.
529
529
  if cursor.description:
@@ -554,35 +554,33 @@ def execute_adhoc(sql: str, conn: Connection, param_values: dict, auto_commit: b
554
554
  committed=True,
555
555
  success=True,
556
556
  )
557
- else:
558
- rows_affected = cursor.rowcount
559
- output_lines = _read_dbms_output(cursor) if capture_dbms_output else []
560
- if auto_commit:
561
- db.commit()
562
- cursor.close()
563
- return AdhocResult(
564
- sql_type=sql_type,
565
- connection_name=conn.name,
566
- sql=original_sql,
567
- db_type=conn.db_type,
568
- rows_affected=rows_affected,
569
- elapsed=elapsed,
570
- committed=True,
571
- output_lines=output_lines,
572
- )
573
- else:
574
- # Caller owns the connection for commit/rollback
575
- owned_db = db
576
- db = None # prevent finally from closing it
577
- return AdhocResult(
578
- sql_type=sql_type,
579
- connection_name=conn.name,
580
- sql=original_sql,
581
- db_type=conn.db_type,
582
- rows_affected=rows_affected,
583
- elapsed=elapsed,
584
- output_lines=output_lines,
585
- ), owned_db
557
+ rows_affected = cursor.rowcount
558
+ output_lines = _read_dbms_output(cursor) if capture_dbms_output else []
559
+ if auto_commit:
560
+ db.commit()
561
+ cursor.close()
562
+ return AdhocResult(
563
+ sql_type=sql_type,
564
+ connection_name=conn.name,
565
+ sql=original_sql,
566
+ db_type=conn.db_type,
567
+ rows_affected=rows_affected,
568
+ elapsed=elapsed,
569
+ committed=True,
570
+ output_lines=output_lines,
571
+ )
572
+ # Caller owns the connection for commit/rollback
573
+ owned_db = db
574
+ db = None # prevent finally from closing it
575
+ return AdhocResult(
576
+ sql_type=sql_type,
577
+ connection_name=conn.name,
578
+ sql=original_sql,
579
+ db_type=conn.db_type,
580
+ rows_affected=rows_affected,
581
+ elapsed=elapsed,
582
+ output_lines=output_lines,
583
+ ), owned_db
586
584
 
587
585
  except Exception as e:
588
586
  return AdhocResult(
@@ -816,7 +814,7 @@ def parse_dml_literals(sql: str) -> dict[str, str]:
816
814
  cols = [c.strip() for c in col_match.group(1).split(",")]
817
815
  raw_vals = re.findall(r"'([^']*)'|(\d+(?:\.\d+)?)", val_match.group(1))
818
816
  vals = [v[0] if v[0] else v[1] for v in raw_vals]
819
- for col, val in zip(cols, vals):
817
+ for col, val in zip(cols, vals, strict=False):
820
818
  literals[col.lower()] = val
821
819
 
822
820
  elif sql_type == "UPDATE":
@@ -13,9 +13,13 @@ the generated statement, the careless paste.
13
13
  from __future__ import annotations
14
14
 
15
15
  import re
16
+ from typing import TYPE_CHECKING
16
17
 
17
18
  import sqlparse
18
19
 
20
+ if TYPE_CHECKING:
21
+ from dbqm.models.connection import Connection
22
+
19
23
  #: What a query-only connection may carry. `EXPLAIN` is here only as a
20
24
  #: prefix — see `_explains_a_query`, which is what decides whether a given
21
25
  #: EXPLAIN is actually read-only.
@@ -52,7 +56,10 @@ def statement_count(sql: str) -> int:
52
56
  and neither is one inside a string literal -- `sqlparse` handles both,
53
57
  which is why the count is not a `str.count(";")`.
54
58
  """
55
- return len([s for s in sqlparse.parse(sql) if s.token_first() is not None])
59
+ return len([
60
+ s for s in sqlparse.parse(sql)
61
+ if s.token_first() is not None # type: ignore[no-untyped-call] # sqlparse ships py.typed, but Statement.token_first() itself is unannotated upstream
62
+ ])
56
63
 
57
64
 
58
65
  def _explains_a_query(sql: str) -> bool:
@@ -76,7 +83,7 @@ def _explains_a_query(sql: str) -> bool:
76
83
  return classify_sql(restante) == "SELECT"
77
84
 
78
85
 
79
- def check_read_only(sql: str, conn) -> None:
86
+ def check_read_only(sql: str, conn: "Connection") -> None:
80
87
  """Raise `ReadOnlyViolation` if `conn` is read-only and `sql` could write.
81
88
 
82
89
  Does nothing when the connection is writable, so callers can invoke it
@@ -127,7 +127,7 @@ def render_template(
127
127
  values: dict[str, str],
128
128
  ) -> str:
129
129
  """Render template content by replacing {{field}} placeholders with values."""
130
- def _replace(match: re.Match) -> str:
130
+ def _replace(match: re.Match[str]) -> str:
131
131
  field_name = match.group(1)
132
132
  return values.get(field_name, match.group(0))
133
133
 
@@ -2,7 +2,7 @@
2
2
  import sys
3
3
 
4
4
 
5
- def main():
5
+ def main() -> None:
6
6
  # Version flag
7
7
  if len(sys.argv) == 2 and sys.argv[1] in ("--version", "-V"):
8
8
  from dbqm._version import __version__
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from dataclasses import dataclass, field, asdict
5
5
  from datetime import datetime
6
- from typing import Optional
6
+ from typing import Any, Optional
7
7
 
8
8
  from dbqm.core.paths import CONFIG_DIR, CONNECTIONS_FILE
9
9
 
@@ -31,12 +31,12 @@ class Connection:
31
31
  read_only: bool = False
32
32
  created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
33
33
 
34
- def to_dict(self) -> dict:
34
+ def to_dict(self) -> dict[str, Any]:
35
35
  d = asdict(self)
36
36
  return {k: v for k, v in d.items() if v is not None}
37
37
 
38
38
  @classmethod
39
- def from_dict(cls, data: dict) -> Connection:
39
+ def from_dict(cls, data: dict[str, Any]) -> Connection:
40
40
  valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
41
41
  filtered = {k: v for k, v in data.items() if k in valid_fields}
42
42
  return cls(**filtered)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  from dataclasses import dataclass, asdict
6
+ from typing import Any
6
7
 
7
8
  from dbqm.core.paths import CONFIG_DIR, SETTINGS_FILE
8
9
 
@@ -20,11 +21,11 @@ class Settings:
20
21
  # tool (e.g. PL/SQL Developer) cannot hijack the 64-bit client dbqm needs.
21
22
  oracle_client_dir: str = ""
22
23
 
23
- def to_dict(self) -> dict:
24
+ def to_dict(self) -> dict[str, Any]:
24
25
  return asdict(self)
25
26
 
26
27
  @classmethod
27
- def from_dict(cls, data: dict) -> Settings:
28
+ def from_dict(cls, data: dict[str, Any]) -> Settings:
28
29
  return cls(
29
30
  audit_log_enabled=data.get("audit_log_enabled", False),
30
31
  theme=data.get("theme", "plano-escuro"),
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import json
5
5
  from dataclasses import dataclass, field, asdict
6
6
  from datetime import datetime
7
- from typing import Optional
7
+ from typing import Any, Optional
8
8
 
9
9
  from dbqm.core.paths import TEMPLATES_DIR, TEMPLATES_FILE
10
10
 
@@ -16,11 +16,11 @@ class Template:
16
16
  content: str # Template text with {{field}} placeholders
17
17
  created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
18
18
 
19
- def to_dict(self) -> dict:
19
+ def to_dict(self) -> dict[str, str]:
20
20
  return asdict(self)
21
21
 
22
22
  @classmethod
23
- def from_dict(cls, data: dict) -> Template:
23
+ def from_dict(cls, data: dict[str, Any]) -> Template:
24
24
  return cls(
25
25
  name=data["name"],
26
26
  description=data.get("description", ""),
@@ -10,7 +10,7 @@ from textual.widgets import Button, DataTable, Input, Select
10
10
  from dbqm.ui.widgets.dialog import Dialog
11
11
 
12
12
 
13
- class ColumnMapsModal(ModalScreen[dict | None]):
13
+ class ColumnMapsModal(ModalScreen[dict[str, dict[str, str]] | None]):
14
14
  """Configure column value mappings (DE-PARA).
15
15
 
16
16
  Dismisses with updated maps dict on save, or None on cancel/ESC.
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from typing import Callable
5
5
 
6
- from textual.app import ComposeResult
6
+ from textual.app import App, ComposeResult
7
7
  from textual.binding import Binding
8
8
  from textual.screen import ModalScreen
9
9
  from textual.widgets import Button
@@ -61,7 +61,7 @@ class ExportPickerModal(ModalScreen[str | None]):
61
61
 
62
62
 
63
63
  def request_export(
64
- app,
64
+ app: App[object],
65
65
  callback: Callable[[str | None], None] | None = None,
66
66
  ) -> None:
67
67
  """Public entry point for the export flow.
@@ -60,7 +60,7 @@ class ParamModal(ModalScreen[dict[str, str] | None]):
60
60
  def __init__(
61
61
  self,
62
62
  query_name: str,
63
- params: list[dict],
63
+ params: list[dict[str, str]],
64
64
  last_values: dict[str, str] | None = None,
65
65
  description: str = "",
66
66
  ) -> None: