dbagent-cli 0.7.0__tar.gz → 0.7.1__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 (81) hide show
  1. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/PKG-INFO +1 -1
  2. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/pipeline.py +122 -0
  4. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/sql/validator.py +55 -0
  5. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/ui/chat_screen.py +12 -4
  6. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/PKG-INFO +1 -1
  7. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/SOURCES.txt +1 -0
  8. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/pyproject.toml +1 -1
  9. dbagent_cli-0.7.1/tests/test_direct_sql.py +83 -0
  10. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/README.md +0 -0
  11. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/confidence.py +0 -0
  12. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/conversation.py +0 -0
  13. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/doctor.py +0 -0
  14. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/error_classifier.py +0 -0
  15. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/generator.py +0 -0
  16. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/recommender.py +0 -0
  17. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/agent/validator.py +0 -0
  18. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/cli.py +0 -0
  19. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/config.py +0 -0
  20. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/connectors/base.py +0 -0
  21. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/connectors/factory.py +0 -0
  22. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/connectors/mongo.py +0 -0
  23. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/connectors/relational.py +0 -0
  24. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/__init__.py +0 -0
  25. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/cache.py +0 -0
  26. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/changeset.py +0 -0
  27. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/cost.py +0 -0
  28. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/recovery.py +0 -0
  29. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/sandbox.py +0 -0
  30. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/execution/streaming.py +0 -0
  31. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/anthropic_provider.py +0 -0
  32. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/auto_setup.py +0 -0
  33. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/base.py +0 -0
  34. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/deepseek_provider.py +0 -0
  35. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/factory.py +0 -0
  36. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/gemini_provider.py +0 -0
  37. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/groq_provider.py +0 -0
  38. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/mistral_provider.py +0 -0
  39. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/mock_provider.py +0 -0
  40. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/ollama_provider.py +0 -0
  41. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/openai_provider.py +0 -0
  42. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/llm/openrouter_provider.py +0 -0
  43. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/cache.py +0 -0
  44. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/formatter.py +0 -0
  45. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/graph.py +0 -0
  46. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/models.py +0 -0
  47. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/retriever.py +0 -0
  48. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/schema/selector.py +0 -0
  49. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/__init__.py +0 -0
  50. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/audit.py +0 -0
  51. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/environment.py +0 -0
  52. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/masking.py +0 -0
  53. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/modes.py +0 -0
  54. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/security/rbac.py +0 -0
  55. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/semantic/__init__.py +0 -0
  56. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/semantic/dictionary.py +0 -0
  57. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/semantic/templates.py +0 -0
  58. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/sql/__init__.py +0 -0
  59. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/sql/optimizer.py +0 -0
  60. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/ui/console.py +0 -0
  61. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/ui/grid_window.py +0 -0
  62. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/ui/viewer.py +0 -0
  63. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent/ui/visualizer.py +0 -0
  64. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  65. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/entry_points.txt +0 -0
  66. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/requires.txt +0 -0
  67. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/dbagent_cli.egg-info/top_level.txt +0 -0
  68. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/setup.cfg +0 -0
  69. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_cache.py +0 -0
  70. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_cli.py +0 -0
  71. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_connectors.py +0 -0
  72. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_doctor_and_viz.py +0 -0
  73. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_generator.py +0 -0
  74. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_graph.py +0 -0
  75. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_providers.py +0 -0
  76. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_recovery.py +0 -0
  77. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_safety.py +0 -0
  78. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_sandbox.py +0 -0
  79. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_schema.py +0 -0
  80. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_security.py +0 -0
  81. {dbagent_cli-0.7.0 → dbagent_cli-0.7.1}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.7.0"
5
+ __version__ = "0.7.1"
6
6
  __author__ = "Santhosh Gupta"
@@ -53,6 +53,7 @@ class PipelineResult:
53
53
  expires_at: Optional[str] = None
54
54
  # Conversational chat response
55
55
  is_chat: bool = False
56
+ is_direct_sql: bool = False
56
57
  message: str = ""
57
58
  # Query classification
58
59
  query_type: str = "unknown" # read, write, ddl, chat, unknown
@@ -199,6 +200,127 @@ class QueryPipeline:
199
200
  result.execution_time_ms = (time.time() - t_start) * 1000
200
201
  return result
201
202
 
203
+ # --- 1.5. Direct Raw SQL Fast-Path (0ms AI latency, 0 tokens) ---
204
+ if SQLValidator.is_direct_sql(user_prompt):
205
+ raw_sql = user_prompt.strip().rstrip(";") + ";"
206
+ result.sql = raw_sql
207
+ result.is_direct_sql = True
208
+ result.query_type = SQLValidator.classify_query(raw_sql)
209
+ result.intent = result.query_type
210
+
211
+ # AST & Security Mode Validation
212
+ is_allowed, violations = SQLValidator.validate_for_mode(raw_sql, mode=self.mode)
213
+ result.safety_violations = violations
214
+ if not is_allowed:
215
+ result.error = f"Security Policy Violation in {self.mode.value} mode: " + "; ".join(violations)
216
+ result.execution_time_ms = (time.time() - t_start) * 1000
217
+ return result
218
+
219
+ # Check Result Cache
220
+ if self.use_cache and not bypass_cache and result.query_type == "read":
221
+ cached = self.result_cache.get(self.db_name, raw_sql)
222
+ if cached:
223
+ cols, rows, age = cached
224
+ result.columns = cols
225
+ result.rows = rows
226
+ result.was_executed = True
227
+ result.is_cached = True
228
+ result.cache_age_seconds = age
229
+ result.execution_time_ms = (time.time() - t_start) * 1000
230
+ result.visualization = ResultVisualizer.analyze_result_shape(cols, rows)
231
+ return result
232
+
233
+ # Decide Execute or Confirm
234
+ should_execute = False
235
+ if force:
236
+ should_execute = True
237
+ elif self.mode in (OperationMode.SAFE, OperationMode.READ_ONLY):
238
+ should_execute = (result.query_type == "read")
239
+ result.was_auto_executed = should_execute
240
+ elif self.mode == OperationMode.CONFIRM:
241
+ if result.query_type == "read" and not violations:
242
+ should_execute = True
243
+ result.was_auto_executed = True
244
+ else:
245
+ result.needs_confirmation = True
246
+ if self.confirm_callback:
247
+ if self.confirm_callback(result.sql, violations):
248
+ should_execute = True
249
+ else:
250
+ result.execution_time_ms = (time.time() - t_start) * 1000
251
+ return result
252
+ else:
253
+ result.execution_time_ms = (time.time() - t_start) * 1000
254
+ return result
255
+ elif self.mode == OperationMode.ADMIN:
256
+ should_execute = True
257
+
258
+ if should_execute:
259
+ # 1. Attempt direct execution without AI (Zero AI Latency)
260
+ cols, rows, error = self.connector.execute_query(raw_sql, limit=100)
261
+ if error is None:
262
+ result.columns = cols
263
+ result.rows = rows
264
+ result.was_executed = True
265
+ result.error = None
266
+ if self.mask_sensitive_data and result.rows:
267
+ result.rows = SensitiveDataMasker.mask_row_set(result.columns, result.rows)
268
+ if result.rows:
269
+ result.visualization = ResultVisualizer.analyze_result_shape(result.columns, result.rows)
270
+ if self.use_cache and result.query_type == "read":
271
+ self.result_cache.set(self.db_name, raw_sql, result.columns, result.rows)
272
+ exec_duration = (time.time() - t_start) * 1000
273
+ result.execution_time_ms = exec_duration
274
+ self.audit_logger.log_execution(
275
+ database=self.db_name,
276
+ user=self.user_id,
277
+ question=user_prompt,
278
+ sql=result.sql,
279
+ mode=self.mode.value,
280
+ status="SUCCESS",
281
+ execution_time_ms=exec_duration,
282
+ rows_returned=len(result.rows),
283
+ risk_level="LOW",
284
+ )
285
+ self.conversation.add_turn(user_prompt=user_prompt, sql=result.sql, is_chat=False)
286
+ return result
287
+
288
+ # 2. Direct query failed with DB error -> Fallback to AI Auto-Repair
289
+ try:
290
+ exact, fuzzy = self.connector.resolve_tables(raw_sql)
291
+ except Exception:
292
+ exact, fuzzy = [], []
293
+ sub_schema, join_steps = self._get_fast_schema_context(raw_sql, exact, fuzzy)
294
+ result.schema = sub_schema
295
+ result.join_steps = join_steps
296
+ repair_prompt = f"The following direct SQL query failed with a database error. Please correct the query using the database schema:\nFailed SQL: {raw_sql}\nError: {error}"
297
+ self._execute_with_retry(result, repair_prompt, sub_schema)
298
+
299
+ if result.success:
300
+ if self.mask_sensitive_data and result.rows:
301
+ result.rows = SensitiveDataMasker.mask_row_set(result.columns, result.rows)
302
+ if result.rows:
303
+ result.visualization = ResultVisualizer.analyze_result_shape(result.columns, result.rows)
304
+ if self.use_cache and result.query_type == "read":
305
+ self.result_cache.set(self.db_name, result.sql, result.columns, result.rows)
306
+ self.conversation.add_turn(user_prompt=user_prompt, sql=result.sql, is_chat=False)
307
+
308
+ exec_duration = (time.time() - t_start) * 1000
309
+ result.execution_time_ms = exec_duration
310
+ self.audit_logger.log_execution(
311
+ database=self.db_name,
312
+ user=self.user_id,
313
+ question=user_prompt,
314
+ sql=result.sql,
315
+ mode=self.mode.value,
316
+ status="SUCCESS" if result.success else "FAILED",
317
+ execution_time_ms=exec_duration,
318
+ rows_returned=len(result.rows),
319
+ risk_level=result.complexity.risk_level if result.complexity else "LOW",
320
+ error=result.error,
321
+ )
322
+ return result
323
+
202
324
  # --- 2. Resolve Tables & Detect Ambiguity ---
203
325
  try:
204
326
  exact, fuzzy = self.connector.resolve_tables(user_prompt)
@@ -52,6 +52,61 @@ class SQLValidator:
52
52
  r"^\s*REVOKE\b",
53
53
  ]
54
54
 
55
+ DIRECT_SQL_REGEX = re.compile(
56
+ r"^\s*(?:"
57
+ r"WITH\b\s+[a-zA-Z0-9_\"`]+\s+AS\b|"
58
+ r"INSERT\s+(?:INTO\s+)?[a-zA-Z0-9_\"`.]+|"
59
+ r"UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET\b|"
60
+ r"DELETE\s+FROM\s+[a-zA-Z0-9_\"`.]+|"
61
+ r"CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|VIEW|DATABASE|SCHEMA|UNIQUE\s+INDEX)\b|"
62
+ r"ALTER\s+(?:TABLE|VIEW)\b|"
63
+ r"DROP\s+(?:TABLE|VIEW|INDEX|DATABASE|SCHEMA)\b|"
64
+ r"TRUNCATE\s+(?:TABLE\s+)?[a-zA-Z0-9_\"`.]+|"
65
+ r"SHOW\s+(?:TABLES|DATABASES|COLUMNS|SCHEMAS|PROCESSLIST|VARIABLES|STATUS|CREATE\s+TABLE)\b|"
66
+ r"DESCRIBE\s+[a-zA-Z0-9_\"`.]+|"
67
+ r"DESC\s+[a-zA-Z0-9_\"`.]+|"
68
+ r"EXPLAIN\s+(?:ANALYZE\s+)?(?:SELECT|INSERT|UPDATE|DELETE|WITH)\b|"
69
+ r"PRAGMA\s+[a-zA-Z0-9_\"`]+"
70
+ r")",
71
+ re.IGNORECASE | re.DOTALL,
72
+ )
73
+
74
+ @classmethod
75
+ def is_direct_sql(cls, text: str) -> bool:
76
+ """
77
+ Determines if the input string is directly a raw SQL query (bypassing AI generation).
78
+ """
79
+ if not text or not text.strip():
80
+ return False
81
+ clean = re.sub(r"--[^\n]*\n?", "", text).strip()
82
+ clean = re.sub(r"/\*.*?\*/", "", clean, flags=re.DOTALL).strip()
83
+ clean_lower = clean.lower()
84
+
85
+ # Check explicit non-SELECT DDL/DML/Utility SQL patterns
86
+ if cls.DIRECT_SQL_REGEX.search(clean):
87
+ return True
88
+
89
+ # Check SELECT statements
90
+ if clean_lower.startswith("select"):
91
+ tokens = clean.split()
92
+ if len(tokens) < 2:
93
+ return False
94
+
95
+ # Natural language conversational words indicate prompt rather than raw SQL
96
+ nl_words = ["the ", " who ", " which ", " that ", " whose ", " please ", " me ", " their ", " those "]
97
+ if any(w in clean_lower for w in nl_words):
98
+ return False
99
+
100
+ # Single expression queries: SELECT 1;, SELECT NOW();, SELECT version();
101
+ if len(tokens) == 2 and (tokens[1].rstrip(";").isdigit() or tokens[1].endswith("()") or tokens[1].startswith("@@")):
102
+ return True
103
+
104
+ # Standard SQL SELECT ... FROM ...
105
+ if " from " in clean_lower:
106
+ return True
107
+
108
+ return False
109
+
55
110
  @classmethod
56
111
  def split_statements(cls, sql: str) -> List[str]:
57
112
  """Splits SQL script into individual statements, ignoring semicolons inside quotes/literals."""
@@ -87,12 +87,17 @@ def render_assistant_turn(
87
87
  ai_model: Optional[str] = None,
88
88
  ) -> None:
89
89
  """Render the assistant response card with SQL, data grid, and execution timing."""
90
- model_display = f" ({ai_model})" if ai_model else ""
91
- timing_str = f" {result.execution_time_ms / 1000:.1f}s" if result.execution_time_ms > 0 else ""
90
+ if result.is_direct_sql and result.retries == 0:
91
+ agent_title = "[bold cyan]🤖 DB-Agent[/bold cyan] [dim](Direct SQL 0 tokens)[/dim]"
92
+ else:
93
+ model_display = f" ({ai_model})" if ai_model else ""
94
+ agent_title = f"[bold cyan]🤖 DB-Agent{model_display}[/bold cyan]"
95
+
96
+ timing_str = f"⚡ {result.execution_time_ms / 1000:.2f}s" if result.execution_time_ms > 0 else ""
92
97
 
93
98
  # Conversational Chat Response
94
99
  if result.is_chat and result.message:
95
- console.print(f"\n[bold cyan]🤖 DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
100
+ console.print(f"\n{agent_title} [dim]{timing_str}[/dim]")
96
101
  console.print(Markdown(result.message))
97
102
  console.print("")
98
103
  return
@@ -119,8 +124,11 @@ def render_assistant_turn(
119
124
 
120
125
  # Executed Result Response
121
126
  if result.was_executed:
122
- console.print(f"\n[bold cyan]🤖 DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
127
+ console.print(f"\n{agent_title} [dim]{timing_str}[/dim]")
123
128
 
129
+ if result.is_direct_sql and result.retries > 0:
130
+ console.print(f"[bold green]✓ Direct SQL failed on database and was automatically fixed by AI ({result.retries} retry)[/bold green]")
131
+
124
132
  if show_sql and result.sql:
125
133
  sql_clean = result.sql.strip()
126
134
  syntax = Syntax(sql_clean, "sql", theme="monokai", line_numbers=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -66,6 +66,7 @@ dbagent_cli.egg-info/top_level.txt
66
66
  tests/test_cache.py
67
67
  tests/test_cli.py
68
68
  tests/test_connectors.py
69
+ tests/test_direct_sql.py
69
70
  tests/test_doctor_and_viz.py
70
71
  tests/test_generator.py
71
72
  tests/test_graph.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbagent-cli"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,83 @@
1
+ """
2
+ Tests for Direct SQL Fast-Path and Self-Healing AI Auto-Correction.
3
+ """
4
+
5
+ from dbagent.sql.validator import SQLValidator
6
+ from dbagent.agent.pipeline import QueryPipeline
7
+ from dbagent.connectors.factory import create_connector
8
+ from dbagent.llm.mock_provider import MockProvider
9
+
10
+
11
+ def test_is_direct_sql_detection():
12
+ # Direct SQL queries
13
+ assert SQLValidator.is_direct_sql("SELECT * FROM users;") is True
14
+ assert SQLValidator.is_direct_sql("select pk_user_id, username from ivis_user where active = 'Y'") is True
15
+ assert SQLValidator.is_direct_sql("SELECT count(*) FROM orders") is True
16
+ assert SQLValidator.is_direct_sql("SELECT 1;") is True
17
+ assert SQLValidator.is_direct_sql("WITH cte AS (SELECT 1) SELECT * FROM cte") is True
18
+ assert SQLValidator.is_direct_sql("INSERT INTO users (id, name) VALUES (1, 'Alice')") is True
19
+ assert SQLValidator.is_direct_sql("UPDATE users SET active = 'Y' WHERE id = 1") is True
20
+ assert SQLValidator.is_direct_sql("DELETE FROM users WHERE id = 1") is True
21
+ assert SQLValidator.is_direct_sql("CREATE TABLE test (id INT);") is True
22
+ assert SQLValidator.is_direct_sql("SHOW TABLES;") is True
23
+ assert SQLValidator.is_direct_sql("DESCRIBE users;") is True
24
+ assert SQLValidator.is_direct_sql("EXPLAIN SELECT * FROM users") is True
25
+ assert SQLValidator.is_direct_sql("PRAGMA table_info(users)") is True
26
+
27
+ # Natural language queries (must NOT be detected as direct SQL)
28
+ assert SQLValidator.is_direct_sql("list top 10 most recent active users") is False
29
+ assert SQLValidator.is_direct_sql("show me the active users") is False
30
+ assert SQLValidator.is_direct_sql("how many users registered today") is False
31
+ assert SQLValidator.is_direct_sql("select the active users who joined last week") is False
32
+ assert SQLValidator.is_direct_sql("hello") is False
33
+
34
+
35
+ def test_pipeline_direct_sql_fast_path():
36
+ # Set up in-memory sqlite database
37
+ connector = create_connector("sqlite:///:memory:")
38
+ connector.execute_query("CREATE TABLE users (id INT, name VARCHAR, active VARCHAR(1));")
39
+ connector.execute_query("INSERT INTO users VALUES (1, 'Alice', 'Y'), (2, 'Bob', 'N');")
40
+
41
+ mock_llm = MockProvider()
42
+ pipeline = QueryPipeline(
43
+ connector=connector,
44
+ llm=mock_llm,
45
+ )
46
+
47
+ # 1. Direct valid query executes without LLM (0 retries)
48
+ result = pipeline.run("SELECT * FROM users WHERE active = 'Y';")
49
+ assert result.was_executed is True
50
+ assert result.is_direct_sql is True
51
+ assert result.retries == 0
52
+ assert len(result.rows) == 1
53
+ assert result.rows[0]["name"] == "Alice"
54
+ assert result.error is None
55
+
56
+ connector.close()
57
+
58
+
59
+ def test_pipeline_direct_sql_auto_repair():
60
+ connector = create_connector("sqlite:///:memory:")
61
+ connector.execute_query("CREATE TABLE users (id INT, name VARCHAR, active VARCHAR(1));")
62
+ connector.execute_query("INSERT INTO users VALUES (1, 'Alice', 'Y'), (2, 'Bob', 'N');")
63
+
64
+ class RepairMockLLM(MockProvider):
65
+ def generate(self, prompt: str, system_prompt: str = None, model: str = None, stream_callback=None) -> str:
66
+ # Emulate AI fixing the typo 'usrs' -> 'users'
67
+ return "SELECT * FROM users WHERE active = 'Y';"
68
+
69
+ pipeline = QueryPipeline(
70
+ connector=connector,
71
+ llm=RepairMockLLM(),
72
+ )
73
+
74
+ # Direct query with typo in table name 'usrs' -> fails initially on DB, then AI auto-repairs to 'users'
75
+ result = pipeline.run("SELECT * FROM usrs WHERE active = 'Y';")
76
+ assert result.was_executed is True
77
+ assert result.is_direct_sql is True
78
+ assert result.retries == 1
79
+ assert "users" in result.sql
80
+ assert len(result.rows) == 1
81
+ assert result.rows[0]["name"] == "Alice"
82
+
83
+ connector.close()
File without changes
File without changes
File without changes