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.
- {dbqm-2.3.0 → dbqm-2.3.2}/PKG-INFO +2 -1
- dbqm-2.3.2/dbqm/_version.py +1 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/query.py +19 -6
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/schema.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/envelope.py +2 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/params.py +17 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/render.py +2 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/audit.py +2 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/db_manager.py +5 -6
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_extractor.py +5 -5
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_mysql.py +3 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/exporter.py +5 -6
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/group_engine.py +3 -3
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/html_report.py +2 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/oracle_client_installer.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/paths.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/query_engine.py +31 -33
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/read_only.py +9 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/template_engine.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/main.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/connection.py +3 -3
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/settings.py +3 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/template.py +3 -3
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/column_maps.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/export_picker.py +2 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/param_input.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/adhoc.py +6 -7
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/browser.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/config_port.py +2 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/connections.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/exec_routine.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/utils.py +2 -2
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/result_table.py +1 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/sql_viewer.py +3 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/PKG-INFO +2 -1
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/requires.txt +1 -0
- dbqm-2.3.2/pyproject.toml +150 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/tests/test_cli_tema.py +1 -0
- dbqm-2.3.0/dbqm/_version.py +0 -1
- dbqm-2.3.0/pyproject.toml +0 -61
- {dbqm-2.3.0 → dbqm-2.3.2}/PYPI.md +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/README.md +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/__main__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/config_bundle.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/connection.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/commands/inspect.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/deps.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/cli/errors.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/config_portability.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/connection_builder.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/crypto.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/ddl_pg.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/history.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/object_browser.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/package_editor.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/core/table_browser.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/design/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/design/tokens.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/group.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/models/query.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/app.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/confirm.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/error.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/export_dir_setup.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/help.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/oracle_client_dir.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/modals/text_input.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_exec.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_manage.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/group_run.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/history.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/oracle_clients.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/package_editor.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/query_exec.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/query_manage.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/settings.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/template_manage.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/screens/tools.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/theme.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/__init__.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/action_bar.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/dialog.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/empty_state.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/group_result.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/hierarchical_list.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/panel.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/progress.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/query_list.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/skeleton.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/status_bar.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/templates_sidebar.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm/ui/widgets/verdict.py +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/SOURCES.txt +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/dependency_links.txt +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/entry_points.txt +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/dbqm.egg-info/top_level.txt +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/setup.cfg +0 -0
- {dbqm-2.3.0 → dbqm-2.3.2}/tests/test_cli.py +0 -0
- {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.
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
453
|
+
if conn.db_type == "sqlserver":
|
|
454
454
|
return get_sqlserver_connection(conn)
|
|
455
|
-
|
|
455
|
+
if conn.db_type == "postgresql":
|
|
456
456
|
return get_postgresql_connection(conn)
|
|
457
|
-
|
|
457
|
+
if conn.db_type == "mysql":
|
|
458
458
|
return get_mysql_connection(conn)
|
|
459
|
-
|
|
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
|
-
|
|
248
|
+
if c_type == "U":
|
|
249
249
|
return f"ALTER TABLE {owner}.{table_name} ADD CONSTRAINT {c_name} UNIQUE ({col_list});"
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
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("'"
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
142
|
-
unique_raw =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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([
|
|
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
|
|
|
@@ -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.
|