natquery 1.0.0__py3-none-any.whl

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 (39) hide show
  1. natquery/__init__.py +0 -0
  2. natquery/cli/__init__.py +0 -0
  3. natquery/cli/commands.py +256 -0
  4. natquery/cli/formatter.py +126 -0
  5. natquery/cli/main.py +51 -0
  6. natquery/cli/shell.py +135 -0
  7. natquery/config/__init__.py +0 -0
  8. natquery/config/connection.py +123 -0
  9. natquery/config/settings.py +100 -0
  10. natquery/execution/__init__.py +0 -0
  11. natquery/execution/engine.py +29 -0
  12. natquery/execution/explain.py +51 -0
  13. natquery/llm/__init__.py +0 -0
  14. natquery/llm/client.py +37 -0
  15. natquery/llm/self_corrector.py +84 -0
  16. natquery/ml/__init__.py +0 -0
  17. natquery/observability/__init__.py +0 -0
  18. natquery/observability/cost_analyzer.py +79 -0
  19. natquery/observability/index_recommender.py +58 -0
  20. natquery/observability/logger.py +109 -0
  21. natquery/observability/performance_api.py +58 -0
  22. natquery/orchestration/__init__.py +0 -0
  23. natquery/orchestration/error_classifier.py +29 -0
  24. natquery/orchestration/error_handler.py +81 -0
  25. natquery/orchestration/pipeline.py +192 -0
  26. natquery/orchestration/workspace.py +44 -0
  27. natquery/prompt/__init__.py +0 -0
  28. natquery/prompt/builder.py +27 -0
  29. natquery/schema/__init__.py +0 -0
  30. natquery/schema/extractor.py +80 -0
  31. natquery/schema/formatter.py +52 -0
  32. natquery/security/__init__.py +0 -0
  33. natquery/security/validator.py +20 -0
  34. natquery-1.0.0.dist-info/METADATA +147 -0
  35. natquery-1.0.0.dist-info/RECORD +39 -0
  36. natquery-1.0.0.dist-info/WHEEL +5 -0
  37. natquery-1.0.0.dist-info/entry_points.txt +2 -0
  38. natquery-1.0.0.dist-info/licenses/LICENSE +21 -0
  39. natquery-1.0.0.dist-info/top_level.txt +1 -0
natquery/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,256 @@
1
+ import json
2
+ from pathlib import Path
3
+ from getpass import getpass
4
+ from urllib.parse import urlparse
5
+ from natquery.config.settings import Settings
6
+ from natquery.observability.performance_api import (
7
+ get_last_run,
8
+ compare_last_two_runs,
9
+ get_slowest_queries,
10
+ )
11
+
12
+
13
+ BASE_DIR = Path.home() / ".natquery"
14
+ CONFIG_FILE = BASE_DIR / "config.json"
15
+
16
+
17
+ def extract_db_name_from_dsn(dsn: str) -> str:
18
+ """
19
+ Extract database name from a PostgreSQL DSN.
20
+ Supports both URL format (postgresql://) and key-value format (dbname=).
21
+ """
22
+ parsed = urlparse(dsn)
23
+
24
+ if parsed.scheme: # URL format
25
+ db_name = parsed.path.lstrip("/").split("?")[
26
+ 0
27
+ ] # Remove leading / and query params
28
+ return db_name if db_name else "unknown_db"
29
+
30
+ # Fallback: key-value DSN (e.g., "dbname=mydb user=postgres host=localhost")
31
+ parts = dsn.split()
32
+ for part in parts:
33
+ if part.startswith("dbname="):
34
+ return part.split("=", 1)[1]
35
+
36
+ return "unknown_db"
37
+
38
+
39
+ def connect_command(dsn_arg=None):
40
+ """
41
+ Setup for NatQuery.
42
+ If dsn_arg is provided, extracts DB name and uses it as workspace.
43
+ Otherwise, runs interactive setup for standard or DSN connections.
44
+ """
45
+
46
+ print("============ NatQuery Setup ============")
47
+
48
+ BASE_DIR.mkdir(exist_ok=True)
49
+
50
+ # If DSN was provided as argument
51
+ if dsn_arg:
52
+ db_name = extract_db_name_from_dsn(dsn_arg)
53
+ workspace_name = db_name
54
+
55
+ print(f"Your Datbase Name: {db_name}")
56
+ print(f"Workspace Name: {workspace_name}")
57
+
58
+ config = {
59
+ "connection_type": "dsn",
60
+ "db_dsn": dsn_arg,
61
+ "db_name": db_name,
62
+ "workspace_name": workspace_name,
63
+ }
64
+
65
+ else:
66
+ db_name = input("Database Name: ").strip()
67
+ workspace_name = db_name
68
+
69
+ db_dir = BASE_DIR / workspace_name
70
+ db_dir.mkdir(parents=True, exist_ok=True)
71
+
72
+ ssl_input = input("Use SSL? (yes/no): ").strip().lower()
73
+ use_ssl = ssl_input in {"yes", "y"}
74
+
75
+ config = {
76
+ "connection_type": "standard",
77
+ "db_host": input("DB Host (e.g. 127.0.0.1): ").strip(),
78
+ "db_port": input("DB Port (e.g. 5432): ").strip(),
79
+ "db_name": db_name,
80
+ "db_user": input("DB User: ").strip(),
81
+ "db_password": getpass("DB Password: "),
82
+ "workspace_name": workspace_name,
83
+ }
84
+
85
+ if use_ssl:
86
+ config["sslmode"] = "require"
87
+
88
+ db_dir = BASE_DIR / workspace_name
89
+ db_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ # LLM config (common to both)
92
+ config.update(
93
+ {
94
+ "llm_provider": input("LLM Provider (groq): ").strip(),
95
+ "llm_api_key": getpass("LLM API Key: "),
96
+ "llm_model": input("LLM Model (e.g. llama-3.1-70b-versatile): ").strip(),
97
+ }
98
+ )
99
+
100
+ config_file = db_dir / "config.json"
101
+
102
+ with open(config_file, "w") as f:
103
+ json.dump(config, f, indent=2)
104
+
105
+ # Set active database
106
+ (BASE_DIR / "current_db").write_text(workspace_name)
107
+
108
+ print(f"\nConfiguration saved for workspace: {workspace_name}")
109
+ print("You can now run: natquery\n")
110
+
111
+
112
+ def reset_command():
113
+ """
114
+ Deletes the active database configuration.
115
+ """
116
+
117
+ try:
118
+ config_path = Settings._get_config_path()
119
+ except RuntimeError:
120
+ print("No active database configured.")
121
+ return
122
+
123
+ if config_path.exists():
124
+ config_path.unlink()
125
+ print("Configuration removed for active database.")
126
+ else:
127
+ print("No configuration found.")
128
+
129
+
130
+ def show_config_command():
131
+ """
132
+ Display current active DB configuration (without secrets).
133
+ """
134
+
135
+ try:
136
+ config = Settings.load_config()
137
+ except RuntimeError:
138
+ print("NatQuery not configured.")
139
+ print("Run: natquery connect")
140
+ return
141
+
142
+ safe_config = config.copy()
143
+
144
+ if "db_password" in safe_config:
145
+ safe_config["db_password"] = "********"
146
+
147
+ if "llm_api_key" in safe_config:
148
+ safe_config["llm_api_key"] = "********"
149
+
150
+ print(json.dumps(safe_config, indent=2))
151
+
152
+
153
+ def select_database_command():
154
+ """
155
+ List all configured databases and allow user to select one as the active database.
156
+ Updates the current_db file without requiring reconnection.
157
+ """
158
+
159
+ BASE_DIR.mkdir(exist_ok=True)
160
+
161
+ # Find all database directories (those containing config.json)
162
+ databases = []
163
+
164
+ if BASE_DIR.exists():
165
+ for item in BASE_DIR.iterdir():
166
+ if item.is_dir() and (item / "config.json").exists():
167
+ databases.append(item.name)
168
+
169
+ if not databases:
170
+ print("No configured databases found.")
171
+ print("Run: natquery connect")
172
+ return
173
+
174
+ # Get current database
175
+ current_db_file = BASE_DIR / "current_db"
176
+ current_db = (
177
+ current_db_file.read_text().strip() if current_db_file.exists() else None
178
+ )
179
+
180
+ # Display databases
181
+ print("\n========== Configured Databases ==========")
182
+ for i, db in enumerate(databases, 1):
183
+ marker = " (current)" if db == current_db else ""
184
+ print(f"{i}. {db}{marker}")
185
+
186
+ print("\n")
187
+
188
+ # Get user selection
189
+ while True:
190
+ try:
191
+ selection = input("Select a database (enter number): ").strip()
192
+ index = int(selection) - 1
193
+
194
+ if 0 <= index < len(databases):
195
+ selected_db = databases[index]
196
+ break
197
+ else:
198
+ print(
199
+ f"Invalid selection. Please enter a number between 1 and {len(databases)}."
200
+ )
201
+ except ValueError:
202
+ print("Invalid input. Please enter a number.")
203
+
204
+ # Update current database
205
+ current_db_file.write_text(selected_db)
206
+ print(f"\nActive database switched to: {selected_db}")
207
+
208
+
209
+ def performance_summary_command():
210
+ db_name = Settings.get_db_config()["dbname"]
211
+
212
+ last = get_last_run(db_name)
213
+
214
+ if not last:
215
+ print("No performance data available.")
216
+ return
217
+
218
+ print("\n====== Performance Summary ======")
219
+ print(f"Execution Time: {last['execution_time_ms']} ms")
220
+ print(f"Total Cost: {last['total_cost']}")
221
+ print(f"Planning Time: {last['planning_time_ms']} ms")
222
+
223
+ print("\n--- Suggestions ---")
224
+ for s in last["suggestions"]:
225
+ print(f"- {s}")
226
+
227
+
228
+ def performance_compare_command():
229
+ db_name = Settings.get_db_config()["dbname"]
230
+
231
+ comp = compare_last_two_runs(db_name)
232
+
233
+ if not comp:
234
+ print("Oops! Not enough data to compare.")
235
+ return
236
+
237
+ print("\n====== Performance Comparison ======")
238
+
239
+ print(f"Execution Time Change: {comp['execution_time_diff']} ms")
240
+ print(f"Cost Change: {comp['cost_diff']}")
241
+
242
+ if comp["execution_time_diff"] < 0:
243
+ print("Booyah! Query improved!")
244
+ else:
245
+ print("Oops! Query got slower.")
246
+
247
+
248
+ def performance_slowest_command():
249
+ db_name = Settings.get_db_config()["dbname"]
250
+
251
+ slow = get_slowest_queries(db_name)
252
+
253
+ print("\n====== Slowest Queries ======")
254
+
255
+ for i, q in enumerate(slow, 1):
256
+ print(f"{i}. {q['execution_time_ms']} ms → {q['sql']}")
@@ -0,0 +1,126 @@
1
+ from rich.table import Table
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from rich import box
5
+ import math
6
+
7
+ console = Console()
8
+
9
+
10
+ def _is_number(value):
11
+ return isinstance(value, (int, float))
12
+
13
+
14
+ def _infer_alignment(data, column):
15
+ """
16
+ Infer alignment based on column values.
17
+ """
18
+ for row in data:
19
+ val = row.get(column)
20
+ if val is not None:
21
+ if _is_number(val):
22
+ return "right"
23
+ else:
24
+ return "left"
25
+ return "left"
26
+
27
+
28
+ def _truncate(text, max_length=50):
29
+ """
30
+ Soft truncate long values (for extremely wide columns).
31
+ """
32
+ text = str(text)
33
+ if len(text) > max_length:
34
+ return text[: max_length - 3] + "..."
35
+ return text
36
+
37
+
38
+ def _format_value(value):
39
+ """
40
+ Clean formatting of cell values.
41
+ """
42
+ if value is None:
43
+ return ""
44
+ return str(value)
45
+
46
+
47
+ def print_table(
48
+ data: list[dict],
49
+ title: str = "#### Result ####",
50
+ page_size: int = 20,
51
+ wrap: bool = True,
52
+ ):
53
+ """
54
+ Advanced table printer:
55
+ - smart alignment
56
+ - pagination
57
+ - wrapping
58
+ - adaptive formatting
59
+ """
60
+
61
+ if not data:
62
+ console.print("[yellow]No results found.[/yellow]")
63
+ return
64
+
65
+ columns = list(data[0].keys())
66
+
67
+ # Special case: single column → vertical list
68
+ if len(columns) == 1:
69
+ col = columns[0]
70
+ console.print(Panel(f"[bold cyan]{col}[/bold cyan]", title=title))
71
+
72
+ for row in data:
73
+ console.print(f"- {row.get(col)}")
74
+
75
+ return
76
+
77
+ total_rows = len(data)
78
+ total_pages = math.ceil(total_rows / page_size)
79
+
80
+ for page in range(total_pages):
81
+
82
+ start = page * page_size
83
+ end = start + page_size
84
+ chunk = data[start:end]
85
+
86
+ table = Table(
87
+ title=f"{title} (Page {page+1}/{total_pages})",
88
+ show_header=True,
89
+ header_style="bold magenta",
90
+ box=box.SIMPLE_HEAVY,
91
+ )
92
+
93
+ # Add columns with smart alignment
94
+ for col in columns:
95
+ justify = _infer_alignment(chunk, col)
96
+
97
+ table.add_column(
98
+ col,
99
+ justify=justify,
100
+ overflow="fold" if wrap else "ellipsis",
101
+ no_wrap=not wrap,
102
+ )
103
+
104
+ # Add rows
105
+ for row in chunk:
106
+ row_values = []
107
+
108
+ for col in columns:
109
+ val = _format_value(row.get(col))
110
+ val = _truncate(val, 80)
111
+ row_values.append(val)
112
+
113
+ table.add_row(*row_values)
114
+
115
+ console.print(table)
116
+
117
+ # Pagination pause (non-blocking for automation)
118
+ if page < total_pages - 1:
119
+ try:
120
+ console.print(
121
+ "[dim]Press Enter for next page... (or Ctrl+C to stop)[/dim]"
122
+ )
123
+ input()
124
+ except (EOFError, KeyboardInterrupt):
125
+ console.print("[yellow]Pagination stopped.[/yellow]")
126
+ break
natquery/cli/main.py ADDED
@@ -0,0 +1,51 @@
1
+ import sys
2
+ from rich import print
3
+ from natquery.cli.commands import (
4
+ connect_command,
5
+ reset_command,
6
+ show_config_command,
7
+ select_database_command,
8
+ )
9
+ from natquery.cli.shell import start_shell
10
+ from natquery.config.settings import Settings
11
+ from natquery.orchestration.workspace import initialize_workspace
12
+
13
+
14
+ def main():
15
+
16
+ if len(sys.argv) > 1:
17
+
18
+ cmd = sys.argv[1]
19
+
20
+ if cmd == "connect":
21
+ # Pass DSN if provided: natquery connect <dsn>
22
+ dsn = sys.argv[2] if len(sys.argv) > 2 else None
23
+ connect_command(dsn)
24
+ return
25
+
26
+ elif cmd == "reset":
27
+ reset_command()
28
+ return
29
+
30
+ elif cmd == "config":
31
+ show_config_command()
32
+ return
33
+
34
+ elif cmd == "select":
35
+ select_database_command()
36
+ return
37
+
38
+ else:
39
+ print(f"Unknown command: {cmd}")
40
+ return
41
+
42
+ # No subcommand → start REPL
43
+
44
+ if not Settings.exists():
45
+ print("NatQuery not configured.")
46
+ print("Run: natquery connect")
47
+ return
48
+
49
+ print("\n[bold cyan]Starting NatQuery...[/bold cyan]\n")
50
+ initialize_workspace()
51
+ start_shell()
natquery/cli/shell.py ADDED
@@ -0,0 +1,135 @@
1
+ from rich import print
2
+ from rich.panel import Panel
3
+ from rich.text import Text
4
+ from rich.console import Console
5
+ from natquery.orchestration.pipeline import run_query
6
+ from natquery.llm.client import generate_sql
7
+ from natquery.cli.formatter import print_table
8
+
9
+ console = Console()
10
+
11
+
12
+ def show_banner():
13
+ banner = Text(
14
+ """
15
+ ███╗ ██╗ █████╗ ████████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗
16
+ ████╗ ██║██╔══██╗╚══██╔══╝██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝
17
+ ██╔██╗ ██║███████║ ██║ ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝
18
+ ██║╚██╗██║██╔══██║ ██║ ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝
19
+ ██║ ╚████║██║ ██║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║
20
+ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
21
+ """,
22
+ style="bold cyan",
23
+ )
24
+
25
+ print(Panel(banner, title="⚡ NatQuery CLI", border_style="green"))
26
+
27
+
28
+ def format_plan_output(summary, suggestions):
29
+ """Format execution plan and index recommendations for display."""
30
+ if not summary and not suggestions:
31
+ print("[dim]No plan data available[/dim]")
32
+ return
33
+
34
+ # Display execution metrics
35
+ if summary:
36
+ metrics = []
37
+ if summary.get("execution_time_ms"):
38
+ metrics.append(f"Execution Time: {summary['execution_time_ms']:.2f}ms")
39
+ if summary.get("planning_time_ms"):
40
+ metrics.append(f"Planning Time: {summary['planning_time_ms']:.2f}ms")
41
+ if summary.get("total_cost"):
42
+ metrics.append(f"Total Cost: {summary['total_cost']:.2f}")
43
+ if summary.get("plan_rows"):
44
+ metrics.append(f"Planned Rows: {summary['plan_rows']}")
45
+
46
+ if metrics:
47
+ print(
48
+ Panel(
49
+ "\n".join(metrics),
50
+ title="Execution Plan",
51
+ border_style="yellow",
52
+ )
53
+ )
54
+
55
+ # Display index recommendations
56
+ if suggestions and len(suggestions) > 0:
57
+ suggestion_lines = []
58
+ for i, sugg in enumerate(suggestions, 1):
59
+ sugg_type = sugg.get("type", "unknown")
60
+ reason = sugg.get("reason", "")
61
+ suggestion_lines.append(f"[bold]{i}. {sugg_type.upper()}[/bold]")
62
+ if reason:
63
+ suggestion_lines.append(f" Reason: {reason}")
64
+ if sugg.get("sql"):
65
+ suggestion_lines.append(f" SQL: {sugg['sql']}")
66
+ suggestion_lines.append("")
67
+
68
+ print(
69
+ Panel(
70
+ "\n".join(suggestion_lines),
71
+ title="Index Recommendations",
72
+ border_style="cyan",
73
+ )
74
+ )
75
+ elif suggestions is not None and len(suggestions) == 0:
76
+ print("[dim]No optimization recommendations[/dim]")
77
+
78
+
79
+ def start_shell():
80
+ show_banner()
81
+ show_sql = False
82
+ show_plan = False
83
+ print("[bold green]NatQuery ready.[/bold green]")
84
+ print(
85
+ "[dim]Commands: /sql (toggle SQL), /plan (toggle execution plan), exit[/dim]\n"
86
+ )
87
+
88
+ while True:
89
+ query = console.input("[bold blue]> [/bold blue]")
90
+
91
+ if query.lower() in ["exit", "quit"]:
92
+ print("[bold red]Goodbye![/bold red]")
93
+ break
94
+
95
+ # Toggle SQL display
96
+ if query.strip() == "/sql":
97
+ show_sql = not show_sql
98
+ status = "ON" if show_sql else "OFF"
99
+ print(f"[yellow]Show SQL:[/yellow] {status}")
100
+ continue
101
+
102
+ # Toggle plan display
103
+ if query.strip() == "/plan":
104
+ show_plan = not show_plan
105
+ status = "ON" if show_plan else "OFF"
106
+ print(f"[yellow]Show Plan:[/yellow] {status}")
107
+ continue
108
+
109
+ if not query.strip():
110
+ continue
111
+
112
+ try:
113
+ query_output = run_query(query)
114
+
115
+ # Handle both old (list) and new (dict) return formats
116
+ if isinstance(query_output, dict):
117
+ result = query_output["result"]
118
+ summary = query_output.get("summary")
119
+ suggestions = query_output.get("suggestions")
120
+ else:
121
+ result = query_output
122
+ summary = None
123
+ suggestions = None
124
+
125
+ if show_sql:
126
+ generated_sql = generate_sql(query)
127
+ print(Panel(generated_sql, title="Generated SQL", border_style="cyan"))
128
+
129
+ print_table(result)
130
+
131
+ if show_plan:
132
+ format_plan_output(summary, suggestions)
133
+
134
+ except Exception as e:
135
+ print(f"[bold red]Error:[/bold red] {e}")
File without changes
@@ -0,0 +1,123 @@
1
+ import psycopg2
2
+ from psycopg2.extensions import connection as PGConnection
3
+ from psycopg2.extras import RealDictCursor
4
+ from natquery.config.settings import Settings
5
+ from natquery.observability.logger import NatQueryLogger
6
+
7
+
8
+ def get_connection() -> PGConnection:
9
+ """
10
+ Creates and returns a PostgreSQL connection.
11
+ Supports:
12
+ - Standard host/port configuration
13
+ - DSN (connection string)
14
+ - SSL (optional)
15
+ """
16
+
17
+ db_config = Settings.get_db_config()
18
+
19
+ # Determine DB name safely (for logging)
20
+ db_name = db_config.get("dbname", "unknown_db")
21
+
22
+ # Log connection attempt
23
+ NatQueryLogger.log_event(
24
+ level="INFO",
25
+ event="db_connection_attempt",
26
+ db_name=db_name,
27
+ conv_id=None,
28
+ details=db_config,
29
+ )
30
+
31
+ try:
32
+
33
+ # ---- DSN MODE ----
34
+ if db_config["type"] == "dsn":
35
+
36
+ conn = psycopg2.connect(
37
+ db_config["dsn"],
38
+ connect_timeout=5,
39
+ )
40
+
41
+ # ---- STANDARD MODE ----
42
+ else:
43
+
44
+ connect_args = {
45
+ "host": db_config["host"],
46
+ "port": db_config["port"],
47
+ "dbname": db_config["dbname"],
48
+ "user": db_config["user"],
49
+ "password": db_config["password"],
50
+ "connect_timeout": 5,
51
+ }
52
+
53
+ # Optional SSL
54
+ sslmode = db_config.get("sslmode")
55
+ if sslmode:
56
+ connect_args["sslmode"] = sslmode
57
+
58
+ conn = psycopg2.connect(**connect_args)
59
+
60
+ conn.autocommit = False
61
+
62
+ # Log success
63
+ NatQueryLogger.log_event(
64
+ level="INFO",
65
+ event="db_connection_success",
66
+ db_name=db_name,
67
+ conv_id=None,
68
+ )
69
+
70
+ return conn
71
+
72
+ except psycopg2.OperationalError as e:
73
+
74
+ # Log failure
75
+ NatQueryLogger.log_event(
76
+ level="ERROR",
77
+ event="db_connection_failed",
78
+ db_name=db_name,
79
+ conv_id=None,
80
+ details={"error": str(e)},
81
+ )
82
+
83
+ raise ConnectionError(
84
+ f"Unable to connect to PostgreSQL server.\nDetails: {e}"
85
+ ) from e
86
+
87
+
88
+ def get_cursor(conn: PGConnection):
89
+ """
90
+ Returns a dictionary-style cursor.
91
+ """
92
+ return conn.cursor(cursor_factory=RealDictCursor)
93
+
94
+
95
+ def close_connection(conn: PGConnection) -> None:
96
+ """
97
+ Safely closes a PostgreSQL connection.
98
+ """
99
+ if conn:
100
+ try:
101
+ # Safely extract db_name; use fallback if connection is partially closed
102
+ try:
103
+ db_name = conn.get_dsn_parameters().get("dbname")
104
+ except (AttributeError, psycopg2.Error):
105
+ db_name = "unknown_db"
106
+
107
+ conn.close()
108
+
109
+ NatQueryLogger.log_event(
110
+ level="INFO",
111
+ event="db_connection_closed",
112
+ db_name=db_name,
113
+ conv_id=None,
114
+ )
115
+
116
+ except Exception as e:
117
+ NatQueryLogger.log_event(
118
+ level="ERROR",
119
+ event="db_connection_close_failed",
120
+ db_name=None,
121
+ conv_id=None,
122
+ details={"error": str(e)},
123
+ )