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.
- natquery/__init__.py +0 -0
- natquery/cli/__init__.py +0 -0
- natquery/cli/commands.py +256 -0
- natquery/cli/formatter.py +126 -0
- natquery/cli/main.py +51 -0
- natquery/cli/shell.py +135 -0
- natquery/config/__init__.py +0 -0
- natquery/config/connection.py +123 -0
- natquery/config/settings.py +100 -0
- natquery/execution/__init__.py +0 -0
- natquery/execution/engine.py +29 -0
- natquery/execution/explain.py +51 -0
- natquery/llm/__init__.py +0 -0
- natquery/llm/client.py +37 -0
- natquery/llm/self_corrector.py +84 -0
- natquery/ml/__init__.py +0 -0
- natquery/observability/__init__.py +0 -0
- natquery/observability/cost_analyzer.py +79 -0
- natquery/observability/index_recommender.py +58 -0
- natquery/observability/logger.py +109 -0
- natquery/observability/performance_api.py +58 -0
- natquery/orchestration/__init__.py +0 -0
- natquery/orchestration/error_classifier.py +29 -0
- natquery/orchestration/error_handler.py +81 -0
- natquery/orchestration/pipeline.py +192 -0
- natquery/orchestration/workspace.py +44 -0
- natquery/prompt/__init__.py +0 -0
- natquery/prompt/builder.py +27 -0
- natquery/schema/__init__.py +0 -0
- natquery/schema/extractor.py +80 -0
- natquery/schema/formatter.py +52 -0
- natquery/security/__init__.py +0 -0
- natquery/security/validator.py +20 -0
- natquery-1.0.0.dist-info/METADATA +147 -0
- natquery-1.0.0.dist-info/RECORD +39 -0
- natquery-1.0.0.dist-info/WHEEL +5 -0
- natquery-1.0.0.dist-info/entry_points.txt +2 -0
- natquery-1.0.0.dist-info/licenses/LICENSE +21 -0
- natquery-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from natquery.llm.self_corrector import correct_sql
|
|
2
|
+
from natquery.security.validator import validate_sql
|
|
3
|
+
from natquery.observability.logger import NatQueryLogger
|
|
4
|
+
from natquery.orchestration.error_classifier import classify_error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def handle_query_error(
|
|
8
|
+
user_query: str,
|
|
9
|
+
sql: str,
|
|
10
|
+
error: Exception,
|
|
11
|
+
conv_id: str,
|
|
12
|
+
db_name: str,
|
|
13
|
+
max_retries: int = 3,
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Retry mechanism with intelligent self-correction.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
current_sql = sql
|
|
20
|
+
last_error = error
|
|
21
|
+
corrected_sql = None
|
|
22
|
+
|
|
23
|
+
for attempt in range(1, max_retries + 1):
|
|
24
|
+
|
|
25
|
+
error_type = classify_error(str(last_error))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
corrected_sql = correct_sql(
|
|
29
|
+
user_query,
|
|
30
|
+
current_sql,
|
|
31
|
+
str(last_error),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if not corrected_sql or len(corrected_sql.strip()) == 0:
|
|
35
|
+
raise ValueError("Empty SQL from self-corrector")
|
|
36
|
+
|
|
37
|
+
NatQueryLogger.log_event(
|
|
38
|
+
level="INFO",
|
|
39
|
+
event="sql_correction_attempt",
|
|
40
|
+
db_name=db_name,
|
|
41
|
+
conv_id=conv_id,
|
|
42
|
+
details={
|
|
43
|
+
"attempt": attempt,
|
|
44
|
+
"error_type": error_type,
|
|
45
|
+
"previous_sql": current_sql,
|
|
46
|
+
"corrected_sql": corrected_sql,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Validate corrected SQL
|
|
51
|
+
validate_sql(corrected_sql)
|
|
52
|
+
|
|
53
|
+
return corrected_sql # success
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
|
|
57
|
+
NatQueryLogger.log_event(
|
|
58
|
+
level="WARNING",
|
|
59
|
+
event="sql_correction_failed",
|
|
60
|
+
db_name=db_name,
|
|
61
|
+
conv_id=conv_id,
|
|
62
|
+
details={
|
|
63
|
+
"attempt": attempt,
|
|
64
|
+
"error_type": error_type,
|
|
65
|
+
"error": str(e),
|
|
66
|
+
"previous_sql": current_sql,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
current_sql = corrected_sql if corrected_sql else current_sql
|
|
71
|
+
last_error = e
|
|
72
|
+
|
|
73
|
+
NatQueryLogger.log_event(
|
|
74
|
+
level="ERROR",
|
|
75
|
+
event="self_correction_exhausted",
|
|
76
|
+
db_name=db_name,
|
|
77
|
+
conv_id=conv_id,
|
|
78
|
+
details={"final_error": str(last_error)},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
raise RuntimeError("Max retries reached. Query failed after corrections.")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from natquery.llm.client import generate_sql
|
|
3
|
+
from natquery.execution.engine import execute_sql
|
|
4
|
+
from natquery.config.settings import Settings
|
|
5
|
+
from natquery.security.validator import validate_sql
|
|
6
|
+
from natquery.observability.logger import NatQueryLogger
|
|
7
|
+
from natquery.orchestration.error_handler import handle_query_error
|
|
8
|
+
from natquery.execution.explain import run_explain_analyze
|
|
9
|
+
from natquery.observability.cost_analyzer import analyze_cost
|
|
10
|
+
from natquery.observability.index_recommender import suggest_indexes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_query(user_query: str):
|
|
14
|
+
|
|
15
|
+
conv_id = NatQueryLogger.generate_conv_id()
|
|
16
|
+
db_name = Settings.get_db_config()["dbname"]
|
|
17
|
+
|
|
18
|
+
# -------- LOG INPUT --------
|
|
19
|
+
NatQueryLogger.log_event(
|
|
20
|
+
level="INFO",
|
|
21
|
+
event="query_received",
|
|
22
|
+
db_name=db_name,
|
|
23
|
+
conv_id=conv_id,
|
|
24
|
+
details={"user_query": user_query},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# -------- LLM STEP --------
|
|
28
|
+
try:
|
|
29
|
+
sql = generate_sql(user_query)
|
|
30
|
+
|
|
31
|
+
if not sql or len(sql.strip()) == 0:
|
|
32
|
+
raise ValueError("Empty SQL generated by LLM")
|
|
33
|
+
|
|
34
|
+
NatQueryLogger.log_event(
|
|
35
|
+
level="INFO",
|
|
36
|
+
event="llm_sql_generated",
|
|
37
|
+
db_name=db_name,
|
|
38
|
+
conv_id=conv_id,
|
|
39
|
+
details={"generated_sql": sql},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
NatQueryLogger.log_event(
|
|
44
|
+
level="ERROR",
|
|
45
|
+
event="llm_generation_failed",
|
|
46
|
+
db_name=db_name,
|
|
47
|
+
conv_id=conv_id,
|
|
48
|
+
details={"error": str(e)},
|
|
49
|
+
)
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
# -------- VALIDATION --------
|
|
53
|
+
try:
|
|
54
|
+
validate_sql(sql)
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
|
|
58
|
+
NatQueryLogger.log_event(
|
|
59
|
+
level="ERROR",
|
|
60
|
+
event="sql_validation_failed",
|
|
61
|
+
db_name=db_name,
|
|
62
|
+
conv_id=conv_id,
|
|
63
|
+
details={
|
|
64
|
+
"error": str(e),
|
|
65
|
+
"generated_sql": sql,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Attempt correction EVEN on validation failure
|
|
70
|
+
sql = handle_query_error(
|
|
71
|
+
user_query,
|
|
72
|
+
sql,
|
|
73
|
+
e,
|
|
74
|
+
conv_id,
|
|
75
|
+
db_name,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# -------- EXECUTION --------
|
|
79
|
+
start_time = time.time()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = execute_sql(sql)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
|
|
86
|
+
NatQueryLogger.log_event(
|
|
87
|
+
level="ERROR",
|
|
88
|
+
event="db_execution_failed",
|
|
89
|
+
db_name=db_name,
|
|
90
|
+
conv_id=conv_id,
|
|
91
|
+
details={
|
|
92
|
+
"error": str(e),
|
|
93
|
+
"generated_sql": sql,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Self-correction loop
|
|
98
|
+
sql = handle_query_error(
|
|
99
|
+
user_query,
|
|
100
|
+
sql,
|
|
101
|
+
e,
|
|
102
|
+
conv_id,
|
|
103
|
+
db_name,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Retry execution once corrected
|
|
107
|
+
try:
|
|
108
|
+
result = execute_sql(sql)
|
|
109
|
+
except Exception as retry_error:
|
|
110
|
+
NatQueryLogger.log_event(
|
|
111
|
+
level="ERROR",
|
|
112
|
+
event="db_execution_failed_after_correction",
|
|
113
|
+
db_name=db_name,
|
|
114
|
+
conv_id=conv_id,
|
|
115
|
+
details={
|
|
116
|
+
"error": str(retry_error),
|
|
117
|
+
"corrected_sql": sql,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
raise retry_error
|
|
121
|
+
|
|
122
|
+
# -------- SUCCESS LOGGING --------
|
|
123
|
+
execution_time_ms = (time.time() - start_time) * 1000
|
|
124
|
+
rows_returned = len(result)
|
|
125
|
+
|
|
126
|
+
NatQueryLogger.log_event(
|
|
127
|
+
level="INFO",
|
|
128
|
+
event="db_execution_completed",
|
|
129
|
+
db_name=db_name,
|
|
130
|
+
conv_id=conv_id,
|
|
131
|
+
details={
|
|
132
|
+
"rows_returned": rows_returned,
|
|
133
|
+
"execution_time_ms": execution_time_ms,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
NatQueryLogger.log_conversation(
|
|
138
|
+
db_name=db_name,
|
|
139
|
+
conv_id=conv_id,
|
|
140
|
+
user_query=user_query,
|
|
141
|
+
generated_sql=sql,
|
|
142
|
+
rows_returned=rows_returned,
|
|
143
|
+
execution_time_ms=execution_time_ms,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# -------- PERFORMANCE ANALYSIS --------
|
|
147
|
+
summary = None
|
|
148
|
+
suggestions = None
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
plan_json = run_explain_analyze(sql)
|
|
152
|
+
|
|
153
|
+
analysis = analyze_cost(plan_json)
|
|
154
|
+
suggestions = suggest_indexes(analysis)
|
|
155
|
+
|
|
156
|
+
summary = analysis["summary"]
|
|
157
|
+
|
|
158
|
+
# Store performance history
|
|
159
|
+
NatQueryLogger.log_performance(
|
|
160
|
+
db_name=db_name,
|
|
161
|
+
conv_id=conv_id,
|
|
162
|
+
sql=sql,
|
|
163
|
+
summary=summary,
|
|
164
|
+
suggestions=suggestions,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Existing log
|
|
168
|
+
NatQueryLogger.log_event(
|
|
169
|
+
level="INFO",
|
|
170
|
+
event="performance_analysis",
|
|
171
|
+
db_name=db_name,
|
|
172
|
+
conv_id=conv_id,
|
|
173
|
+
details={
|
|
174
|
+
"summary": summary,
|
|
175
|
+
"suggestions": suggestions,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
NatQueryLogger.log_event(
|
|
181
|
+
level="ERROR",
|
|
182
|
+
event="performance_analysis_failed",
|
|
183
|
+
db_name=db_name,
|
|
184
|
+
conv_id=conv_id,
|
|
185
|
+
details={"error": str(e)},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"result": result,
|
|
190
|
+
"summary": summary,
|
|
191
|
+
"suggestions": suggestions,
|
|
192
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
from natquery.config.connection import get_connection, close_connection
|
|
4
|
+
from natquery.schema.extractor import extract_schema
|
|
5
|
+
from natquery.observability.logger import NatQueryLogger
|
|
6
|
+
from natquery.config.settings import Settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def initialize_workspace():
|
|
10
|
+
"""
|
|
11
|
+
Ensures schema is extracted and saved under:
|
|
12
|
+
~/.natquery/<db_name>/schema.json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
db_config = Settings.get_db_config()
|
|
16
|
+
db_name = db_config["dbname"]
|
|
17
|
+
|
|
18
|
+
base_dir = Path.home() / ".natquery"
|
|
19
|
+
workspace_dir = base_dir / db_name
|
|
20
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
schema_file = workspace_dir / "schema.json"
|
|
23
|
+
|
|
24
|
+
# Skip if already extracted
|
|
25
|
+
if schema_file.exists():
|
|
26
|
+
print("Schema already exists. Skipping extraction.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
conn = get_connection()
|
|
30
|
+
schema = extract_schema(conn)
|
|
31
|
+
close_connection(conn)
|
|
32
|
+
|
|
33
|
+
with open(schema_file, "w") as f:
|
|
34
|
+
json.dump(schema, f, indent=2)
|
|
35
|
+
|
|
36
|
+
NatQueryLogger.log_event(
|
|
37
|
+
level="INFO",
|
|
38
|
+
event="schema_extracted",
|
|
39
|
+
db_name=db_name,
|
|
40
|
+
conv_id=None,
|
|
41
|
+
details={"schema_path": str(schema_file)},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
print(f"Schema saved to {schema_file}")
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
def build_prompt(user_query: str, schema_text: str) -> list:
|
|
2
|
+
"""
|
|
3
|
+
Build structured messages for LLM.
|
|
4
|
+
Returns messages list (for chat APIs).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
system_prompt = f"""
|
|
8
|
+
You are a PostgreSQL expert.
|
|
9
|
+
|
|
10
|
+
STRICT RULES:
|
|
11
|
+
- Generate ONLY valid PostgreSQL SQL.
|
|
12
|
+
- DO NOT include explanations.
|
|
13
|
+
- DO NOT use INSERT, UPDATE, DELETE, DROP.
|
|
14
|
+
- Use only tables and columns from the schema.
|
|
15
|
+
- Prefer explicit JOINs using foreign keys.
|
|
16
|
+
- If query is ambiguous, choose the most relevant interpretation.
|
|
17
|
+
|
|
18
|
+
DATABASE SCHEMA:
|
|
19
|
+
{schema_text}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
messages = [
|
|
23
|
+
{"role": "system", "content": system_prompt.strip()},
|
|
24
|
+
{"role": "user", "content": user_query.strip()},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
return messages
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
def extract_schema(conn):
|
|
2
|
+
"""
|
|
3
|
+
Extract full schema:
|
|
4
|
+
- tables
|
|
5
|
+
- columns + types
|
|
6
|
+
- primary keys
|
|
7
|
+
- foreign keys
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
cursor = conn.cursor()
|
|
11
|
+
|
|
12
|
+
schema = {"tables": {}}
|
|
13
|
+
|
|
14
|
+
# Get tables
|
|
15
|
+
cursor.execute(
|
|
16
|
+
"""
|
|
17
|
+
SELECT table_name
|
|
18
|
+
FROM information_schema.tables
|
|
19
|
+
WHERE table_schema = 'public';
|
|
20
|
+
"""
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
24
|
+
|
|
25
|
+
for table in tables:
|
|
26
|
+
schema["tables"][table] = {"columns": {}, "primary_key": [], "foreign_keys": []}
|
|
27
|
+
|
|
28
|
+
# Columns and types
|
|
29
|
+
cursor.execute(
|
|
30
|
+
"""
|
|
31
|
+
SELECT column_name, data_type
|
|
32
|
+
FROM information_schema.columns
|
|
33
|
+
WHERE table_name = %s;
|
|
34
|
+
""",
|
|
35
|
+
(table,),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for col, dtype in cursor.fetchall():
|
|
39
|
+
schema["tables"][table]["columns"][col] = dtype
|
|
40
|
+
|
|
41
|
+
# Primary keys
|
|
42
|
+
cursor.execute(
|
|
43
|
+
"""
|
|
44
|
+
SELECT kcu.column_name
|
|
45
|
+
FROM information_schema.table_constraints tc
|
|
46
|
+
JOIN information_schema.key_column_usage kcu
|
|
47
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
48
|
+
WHERE tc.table_name = %s
|
|
49
|
+
AND tc.constraint_type = 'PRIMARY KEY';
|
|
50
|
+
""",
|
|
51
|
+
(table,),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
schema["tables"][table]["primary_key"] = [row[0] for row in cursor.fetchall()]
|
|
55
|
+
|
|
56
|
+
# Foreign keys
|
|
57
|
+
cursor.execute(
|
|
58
|
+
"""
|
|
59
|
+
SELECT
|
|
60
|
+
kcu.column_name,
|
|
61
|
+
ccu.table_name AS foreign_table,
|
|
62
|
+
ccu.column_name AS foreign_column
|
|
63
|
+
FROM information_schema.table_constraints tc
|
|
64
|
+
JOIN information_schema.key_column_usage kcu
|
|
65
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
66
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
67
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
68
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
69
|
+
AND tc.table_name = %s;
|
|
70
|
+
""",
|
|
71
|
+
(table,),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
for col, ref_table, ref_col in cursor.fetchall():
|
|
75
|
+
schema["tables"][table]["foreign_keys"].append(
|
|
76
|
+
{"column": col, "references": {"table": ref_table, "column": ref_col}}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
cursor.close()
|
|
80
|
+
return schema
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
def format_schema(schema: dict) -> str:
|
|
2
|
+
"""
|
|
3
|
+
Convert schema JSON → compact SQL-like text for LLM.
|
|
4
|
+
|
|
5
|
+
RAG-ready:
|
|
6
|
+
- Works with full schema OR filtered schema
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
if not schema or "tables" not in schema:
|
|
10
|
+
return ""
|
|
11
|
+
|
|
12
|
+
lines = []
|
|
13
|
+
|
|
14
|
+
tables = schema["tables"]
|
|
15
|
+
|
|
16
|
+
# Ensure deterministic ordering
|
|
17
|
+
for table_name in sorted(tables.keys()):
|
|
18
|
+
table = tables[table_name]
|
|
19
|
+
|
|
20
|
+
columns = table.get("columns", {})
|
|
21
|
+
pk = set(table.get("primary_key", []))
|
|
22
|
+
|
|
23
|
+
col_lines = []
|
|
24
|
+
|
|
25
|
+
for col_name in columns:
|
|
26
|
+
col_type = columns[col_name].upper()
|
|
27
|
+
|
|
28
|
+
if col_name in pk:
|
|
29
|
+
col_lines.append(f" {col_name} {col_type} PRIMARY KEY")
|
|
30
|
+
else:
|
|
31
|
+
col_lines.append(f" {col_name} {col_type}")
|
|
32
|
+
|
|
33
|
+
table_block = f"TABLE {table_name} (\n" + ",\n".join(col_lines) + "\n)"
|
|
34
|
+
lines.append(table_block)
|
|
35
|
+
|
|
36
|
+
# Relationships (FKs)
|
|
37
|
+
relationships = []
|
|
38
|
+
|
|
39
|
+
for table_name in sorted(tables.keys()):
|
|
40
|
+
fks = tables[table_name].get("foreign_keys", [])
|
|
41
|
+
|
|
42
|
+
for fk in fks:
|
|
43
|
+
relationships.append(
|
|
44
|
+
f"{table_name}.{fk['column']} → "
|
|
45
|
+
f"{fk['references']['table']}.{fk['references']['column']}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if relationships:
|
|
49
|
+
lines.append("\nRELATIONSHIPS:")
|
|
50
|
+
lines.extend(relationships)
|
|
51
|
+
|
|
52
|
+
return "\n\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def validate_sql(sql: str):
|
|
5
|
+
"""
|
|
6
|
+
Ensure SQL is safe (SELECT only).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
sql_clean = sql.strip().lower()
|
|
10
|
+
|
|
11
|
+
if not sql_clean.startswith("select"):
|
|
12
|
+
raise ValueError("Only SELECT queries are allowed.")
|
|
13
|
+
|
|
14
|
+
forbidden = ["insert", "update", "delete", "drop", "alter", "truncate"]
|
|
15
|
+
|
|
16
|
+
for word in forbidden:
|
|
17
|
+
if re.search(rf"\b{word}\b", sql_clean):
|
|
18
|
+
raise ValueError(f"Forbidden keyword detected: {word}")
|
|
19
|
+
|
|
20
|
+
return True
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: natquery
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Interact with PostgreSQL databases using natural language via a secure, performance-aware CLI
|
|
5
|
+
Author-email: Tanmai Raghava <tanmairaghava@kgpian.iitkgp.ac.in>, Krishna Chaitanya <krish89@kgpian.iitkgp.ac.in>, Thania Joshi <thaniavjoshi@kgpian.iitkgp.ac.in>, Lokeswara Reddy <lokeswarareddy2006@gmail.com>, Manasa Vankam <vankammanasa45@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/N-T-Raghava/NatQuery
|
|
8
|
+
Project-URL: Documentation, https://github.com/N-T-Raghava/NatQuery#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/N-T-Raghava/NatQuery
|
|
10
|
+
Project-URL: Issues, https://github.com/N-T-Raghava/NatQuery/issues
|
|
11
|
+
Keywords: nlp,database,postgres,natural-language,cli,llm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
29
|
+
Requires-Dist: groq>=0.4.0
|
|
30
|
+
Requires-Dist: rich>=13.0.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: black==25.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff==0.15.6; extra == "dev"
|
|
36
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# NatQuery
|
|
41
|
+
natquery is a Python package that enables users to interact with relational databases using natural language via a secure, performance-aware command-line interface along with database driven machine learning analytics.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
natquery/
|
|
45
|
+
│
|
|
46
|
+
├── pyproject.toml
|
|
47
|
+
├── README.md
|
|
48
|
+
├── requirements.txt
|
|
49
|
+
│── src/
|
|
50
|
+
├ ├── natquery/
|
|
51
|
+
│ ├── __init__.py
|
|
52
|
+
│
|
|
53
|
+
│ # =========================
|
|
54
|
+
│ # 1. CLI LAYER
|
|
55
|
+
│ # =========================
|
|
56
|
+
│ ├── cli/
|
|
57
|
+
│ │ ├── __init__.py
|
|
58
|
+
│ │ ├── main.py # Entry point
|
|
59
|
+
│ │ ├── shell.py # Interactive REPL
|
|
60
|
+
│ │ ├── commands.py # CLI command registry
|
|
61
|
+
│
|
|
62
|
+
│ # =========================
|
|
63
|
+
│ # 2. CONFIG LAYER
|
|
64
|
+
│ # =========================
|
|
65
|
+
│ ├── config/
|
|
66
|
+
│ │ ├── __init__.py
|
|
67
|
+
│ │ ├── settings.py # API keys, DB config
|
|
68
|
+
│ │ ├── connection.py # PostgreSQL connection manager
|
|
69
|
+
│
|
|
70
|
+
│ # =========================
|
|
71
|
+
│ # 3. ORCHESTRATION LAYER
|
|
72
|
+
│ # =========================
|
|
73
|
+
│ ├── orchestration/
|
|
74
|
+
│ │ ├── __init__.py
|
|
75
|
+
│ │ ├── pipeline.py # End-to-end coordinator
|
|
76
|
+
│ │ ├── error_handler.py
|
|
77
|
+
│
|
|
78
|
+
│ # =========================
|
|
79
|
+
│ # 4. SCHEMA MODULE
|
|
80
|
+
│ # =========================
|
|
81
|
+
│ ├── schema/
|
|
82
|
+
│ │ ├── __init__.py
|
|
83
|
+
│ │ ├── extractor.py # Dynamic schema extraction
|
|
84
|
+
│ │ ├── formatter.py # Convert schema to prompt-friendly text
|
|
85
|
+
│
|
|
86
|
+
│ # =========================
|
|
87
|
+
│ # 5. PROMPT BUILDER
|
|
88
|
+
│ # =========================
|
|
89
|
+
│ ├── prompt/
|
|
90
|
+
│ │ ├── __init__.py
|
|
91
|
+
│ │ ├── builder.py # Structured prompt construction
|
|
92
|
+
│
|
|
93
|
+
│ # =========================
|
|
94
|
+
│ # 6. LLM CLIENT
|
|
95
|
+
│ # =========================
|
|
96
|
+
│ ├── llm/
|
|
97
|
+
│ │ ├── __init__.py
|
|
98
|
+
│ │ ├── client.py # LLM API calls
|
|
99
|
+
│ │ ├── self_corrector.py # Retry & correction mechanism
|
|
100
|
+
│
|
|
101
|
+
│ # =========================
|
|
102
|
+
│ # 7. SECURITY + VALIDATION
|
|
103
|
+
│ # =========================
|
|
104
|
+
│ ├── security/
|
|
105
|
+
│ │ ├── __init__.py
|
|
106
|
+
│ │ ├── validator.py # SELECT-only enforcement
|
|
107
|
+
│ │ ├── limiter.py # LIMIT enforcement
|
|
108
|
+
│
|
|
109
|
+
│ # =========================
|
|
110
|
+
│ # 8. EXECUTION ENGINE
|
|
111
|
+
│ # =========================
|
|
112
|
+
│ ├── execution/
|
|
113
|
+
│ │ ├── __init__.py
|
|
114
|
+
│ │ ├── engine.py # SQL execution
|
|
115
|
+
│ │ ├── explain.py # EXPLAIN / ANALYZE
|
|
116
|
+
│
|
|
117
|
+
│ # =========================
|
|
118
|
+
│ # 9. MACHINE LEARNING
|
|
119
|
+
│ # =========================
|
|
120
|
+
│ ├── ml/
|
|
121
|
+
│ │ ├── __init__.py
|
|
122
|
+
│ │ ├── preprocessing.py
|
|
123
|
+
│ │ ├── trainer.py
|
|
124
|
+
│ │ ├── inference.py
|
|
125
|
+
│ │
|
|
126
|
+
│ # =========================
|
|
127
|
+
│ # 10. PERFORMANCE + LOGGING
|
|
128
|
+
│ # =========================
|
|
129
|
+
│ ├── observability/
|
|
130
|
+
│ ├── __init__.py
|
|
131
|
+
│ ├── logger.py
|
|
132
|
+
│ ├── cost_analyzer.py
|
|
133
|
+
│ ├── index_recommender.py
|
|
134
|
+
│
|
|
135
|
+
│
|
|
136
|
+
├── database/
|
|
137
|
+
│ ├── schema.sql
|
|
138
|
+
│ ├── seed.py
|
|
139
|
+
│ ├── synthetic_data.py
|
|
140
|
+
│
|
|
141
|
+
├── tests/
|
|
142
|
+
│ ├── test_pipeline.py
|
|
143
|
+
│ ├── test_security.py
|
|
144
|
+
│ ├── test_ml.py
|
|
145
|
+
│
|
|
146
|
+
└── benchmarks/
|
|
147
|
+
├── nl_queries.json
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
natquery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
natquery/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
natquery/cli/commands.py,sha256=tzTKTRT3yiL_LsgGV6rOj7JXYoWnp40oLb-x3-BBJ3I,7103
|
|
4
|
+
natquery/cli/formatter.py,sha256=qK2B9ANzNbFbcCT6QAEdM8rm0AZbWezCL333_Jf7sPM,2981
|
|
5
|
+
natquery/cli/main.py,sha256=FJAllKEqrqARZfH-qArt8UbWgk3GlAZ56LnaOKmGSRM,1189
|
|
6
|
+
natquery/cli/shell.py,sha256=Lq5UHDENCFXKugcUvzn7PfIgbKdH99xHaT-U6JPWdTc,5159
|
|
7
|
+
natquery/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
natquery/config/connection.py,sha256=7t-Ia4PWkXT6I3tZKFrUOB78aLGJjNNY8wrZSFVl9Cc,3212
|
|
9
|
+
natquery/config/settings.py,sha256=ZtwGLI3kGyPpaKUsPkIaKvOh47OKjUKGzQ1mCc9sEq0,2723
|
|
10
|
+
natquery/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
natquery/execution/engine.py,sha256=eviZj8QE4CtrLPIzluTntb1KuCDpMWiu7WJ14VjGC6E,750
|
|
12
|
+
natquery/execution/explain.py,sha256=aEcuYDnjxruKrmlQlMkeJCvmGyZxElVMCQ42EjC3vvs,1207
|
|
13
|
+
natquery/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
natquery/llm/client.py,sha256=ui6AGI7ApT1YBCceXEtQzl13Gb3hJiLZ8DwJZhb4Jjw,995
|
|
15
|
+
natquery/llm/self_corrector.py,sha256=kOn1_pcBEVc7eW6L-Y-IL1xiRh7GhvF6eGx6WdCcH8U,2211
|
|
16
|
+
natquery/ml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
natquery/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
natquery/observability/cost_analyzer.py,sha256=zVZoDuYYvmPMkkM-PZ6mXbGQjjBb5ICIeZKw904jzyY,2063
|
|
19
|
+
natquery/observability/index_recommender.py,sha256=nOZCL69m06LL0rnELRgH6sdDGPqhLC1D3TsVwq_QY4g,1910
|
|
20
|
+
natquery/observability/logger.py,sha256=uIRfeSBQA5lrq0k3YGydVjUihMcGllOn-hLYSxn--L4,2756
|
|
21
|
+
natquery/observability/performance_api.py,sha256=5cNJ8fowM_WWljN7wEUf_1Yhqimsi1Mmvh8LiS0vQcc,1257
|
|
22
|
+
natquery/orchestration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
natquery/orchestration/error_classifier.py,sha256=ok0e2TQRZaTN9mvfe7pgiPFd0iWxrq8PhYjXpjzqNsQ,711
|
|
24
|
+
natquery/orchestration/error_handler.py,sha256=TB_uwYp9Crg0p1-JV23OvIzFKIHLIapAvL_003Od-og,2299
|
|
25
|
+
natquery/orchestration/pipeline.py,sha256=Y_D4RTrbR0sy3eISYgUxUplAX4smlKqMzrxeLn1Ibu4,5012
|
|
26
|
+
natquery/orchestration/workspace.py,sha256=FMyt-iVZGfYaRF0slO7HlFH3BzmtAr8uItXfi83jQkY,1203
|
|
27
|
+
natquery/prompt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
natquery/prompt/builder.py,sha256=K3KMgvfHyE5kxKD1tsFGNsw4aTwQy0H5DN94EPYlIHc,698
|
|
29
|
+
natquery/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
natquery/schema/extractor.py,sha256=PMlGI7d_0QvDz8vo8EsU23pol1kO91mx2ZFfcBvWGZA,2271
|
|
31
|
+
natquery/schema/formatter.py,sha256=xSduYN37k0Zp1PrTS0iiMXJm_HsdwuQzz5-BPvXln_U,1388
|
|
32
|
+
natquery/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
natquery/security/validator.py,sha256=GOOnfzpTrIX8tMQ8RWkSsuzgbYqErOeh5xfpDhUBNnA,475
|
|
34
|
+
natquery-1.0.0.dist-info/licenses/LICENSE,sha256=k8l1DTpell6239gvuFh5bHDfMl7VH49BTGNmMYC8fkA,1071
|
|
35
|
+
natquery-1.0.0.dist-info/METADATA,sha256=QTMNp06Sks0xdh-uEgFhixCHv87qCNzh887GeJb0X6U,5634
|
|
36
|
+
natquery-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
37
|
+
natquery-1.0.0.dist-info/entry_points.txt,sha256=B0v2hqxmh3gfiYPmhEp-o8DSOt1YTXOAnjNffMLQCCU,52
|
|
38
|
+
natquery-1.0.0.dist-info/top_level.txt,sha256=eX578Cs4V48pAKXMICMKMMU0L_UFz_KLV1FHvLxZEP8,9
|
|
39
|
+
natquery-1.0.0.dist-info/RECORD,,
|