dbagent-cli 0.11.0__tar.gz → 0.11.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 (96) hide show
  1. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/PKG-INFO +1 -1
  2. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/generator.py +4 -0
  4. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/pipeline.py +16 -8
  5. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/prompt_queue.py +17 -5
  6. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/validator.py +12 -6
  7. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/console.py +2 -2
  8. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/PKG-INFO +1 -1
  9. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/SOURCES.txt +1 -0
  10. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/pyproject.toml +1 -1
  11. dbagent_cli-0.11.1/tests/test_prompt_queue_and_sql.py +48 -0
  12. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/README.md +0 -0
  13. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/confidence.py +0 -0
  14. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/conversation.py +0 -0
  15. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/doctor.py +0 -0
  16. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/error_classifier.py +0 -0
  17. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/recommender.py +0 -0
  18. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/validator.py +0 -0
  19. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/benchmark.py +0 -0
  20. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/cli.py +0 -0
  21. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/config.py +0 -0
  22. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/base.py +0 -0
  23. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/factory.py +0 -0
  24. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/mongo.py +0 -0
  25. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/relational.py +0 -0
  26. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/errors.py +0 -0
  27. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/__init__.py +0 -0
  28. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/cache.py +0 -0
  29. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/changeset.py +0 -0
  30. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/cost.py +0 -0
  31. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/recovery.py +0 -0
  32. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/sandbox.py +0 -0
  33. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/streaming.py +0 -0
  34. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/anthropic_provider.py +0 -0
  35. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/auto_setup.py +0 -0
  36. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/base.py +0 -0
  37. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/deepseek_provider.py +0 -0
  38. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/factory.py +0 -0
  39. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/gemini_provider.py +0 -0
  40. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/groq_provider.py +0 -0
  41. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/mistral_provider.py +0 -0
  42. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/mock_provider.py +0 -0
  43. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/ollama_provider.py +0 -0
  44. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/openai_provider.py +0 -0
  45. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/openrouter_provider.py +0 -0
  46. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/plugins.py +0 -0
  47. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sample_db.py +0 -0
  48. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/cache.py +0 -0
  49. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/formatter.py +0 -0
  50. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/graph.py +0 -0
  51. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/ground_truth.py +0 -0
  52. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/models.py +0 -0
  53. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/retriever.py +0 -0
  54. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/selector.py +0 -0
  55. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/__init__.py +0 -0
  56. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/audit.py +0 -0
  57. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/environment.py +0 -0
  58. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/masking.py +0 -0
  59. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/modes.py +0 -0
  60. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/rbac.py +0 -0
  61. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/__init__.py +0 -0
  62. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/dictionary.py +0 -0
  63. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/knowledge.py +0 -0
  64. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/templates.py +0 -0
  65. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/__init__.py +0 -0
  66. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/optimizer.py +0 -0
  67. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/chat_screen.py +0 -0
  68. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/grid_window.py +0 -0
  69. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/live_streamer.py +0 -0
  70. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/viewer.py +0 -0
  71. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/visualizer.py +0 -0
  72. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  73. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/entry_points.txt +0 -0
  74. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/requires.txt +0 -0
  75. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/top_level.txt +0 -0
  76. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/setup.cfg +0 -0
  77. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_benchmarks.py +0 -0
  78. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_cache.py +0 -0
  79. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_cli.py +0 -0
  80. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_connectors.py +0 -0
  81. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_demo_and_sample_db.py +0 -0
  82. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_direct_sql.py +0 -0
  83. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_doctor_and_viz.py +0 -0
  84. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_formats_and_exit_codes.py +0 -0
  85. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_generator.py +0 -0
  86. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_graph.py +0 -0
  87. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_knowledge.py +0 -0
  88. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_plugins.py +0 -0
  89. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_prompt_queue.py +0 -0
  90. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_providers.py +0 -0
  91. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_recovery.py +0 -0
  92. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_safety.py +0 -0
  93. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_sandbox.py +0 -0
  94. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_schema.py +0 -0
  95. {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_security.py +0 -0
  96. {dbagent_cli-0.11.0 → dbagent_cli-0.11.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.11.0
3
+ Version: 0.11.1
4
4
  Summary: The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)
5
5
  Author: Santhosh Gupta
6
6
  License: MIT
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.11.0"
5
+ __version__ = "0.11.1"
6
6
  __author__ = "Santhosh Gupta"
@@ -59,6 +59,10 @@ Generate ONLY the raw executable SQL query for the given database. No markdown f
59
59
  - Check the column data type and sample values shown in the schema.
60
60
  - If the column is VARCHAR(1) or CHAR(1) or text, check if values use 'Y'/'N', 'YES'/'NO', '1'/'0', 'ACTIVE'/'INACTIVE', 'TRUE'/'FALSE'.
61
61
  - If sample values show 'Y'/'N' (or if it's a single-character VARCHAR/CHAR column), use `active = 'Y'` or `active IN ('Y', '1', 'true', 'TRUE')` for active/enabled status.
62
+ 10. For Deletions & Cascading Mapping Cleanup (when deleting a record/user without foreign key constraint violations):
63
+ - For PostgreSQL: Use a single Common Table Expression (CTE) with data-modifying statements deleting from all child, mapping, activity, profile, and preference tables first, and finally deleting from the parent table (e.g. `WITH target AS (SELECT pk_user_id FROM ivis_user WHERE ...), del_act AS (DELETE FROM user_activity WHERE user_id IN (SELECT pk_user_id FROM target)), del_map AS (DELETE FROM ivis_user_access WHERE fk_user_id IN (SELECT pk_user_id FROM target)) DELETE FROM ivis_user WHERE pk_user_id IN (SELECT pk_user_id FROM target);`).
64
+ - For MySQL / SQLite: Generate clean, ordered DELETE statements deleting referencing child records before the parent record.
65
+ - Always delete from all referencing tables present in the schema to avoid foreign key violations.
62
66
  """
63
67
 
64
68
 
@@ -160,6 +160,9 @@ class QueryPipeline:
160
160
 
161
161
  # 2. Targeted on-demand table introspection with automatic junction expansion
162
162
  target_names = list(set(exact + fuzzy))
163
+ is_delete_or_mapping = any(w in user_prompt.lower() for w in ["delete", "remove", "drop", "cascade", "mapping", "map", "link", "clean", "without any mappings"])
164
+ max_tables_limit = 25 if is_delete_or_mapping else 12
165
+
163
166
  if target_names:
164
167
  try:
165
168
  all_table_names = self.connector.get_table_names()
@@ -174,24 +177,29 @@ class QueryPipeline:
174
177
  t_lower = t.lower()
175
178
  if t in target_names:
176
179
  continue
177
- is_map = any(m in t_lower for m in ["_map", "mapping", "_rel", "_junction", "_link", "_access", "_config"])
180
+ is_map = any(m in t_lower for m in ["_map", "mapping", "_rel", "_junction", "_link", "_access", "_config", "_preference", "_profile", "_activity", "_log"])
178
181
  matches_stem = any(s in t_lower for s in stems)
179
- if (is_map and matches_stem) or sum(1 for s in stems if s in t_lower) >= 2:
180
- if t not in target_names:
181
- target_names.append(t)
182
- if len(target_names) >= 12:
182
+ if is_delete_or_mapping:
183
+ if matches_stem or (is_map and matches_stem) or sum(1 for s in stems if s in t_lower) >= 1:
184
+ if t not in target_names:
185
+ target_names.append(t)
186
+ else:
187
+ if (is_map and matches_stem) or sum(1 for s in stems if s in t_lower) >= 2:
188
+ if t not in target_names:
189
+ target_names.append(t)
190
+ if len(target_names) >= max_tables_limit:
183
191
  break
184
192
  except Exception:
185
193
  pass
186
194
 
187
195
  targeted = self.connector.inspect_schema(table_names=target_names, include_samples=True, max_samples=2)
188
196
  retriever = ContextRetriever(targeted)
189
- return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=12)
197
+ return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=max_tables_limit)
190
198
 
191
199
  # 3. Fallback: inspect targeted
192
- targeted = self.connector.inspect_targeted(user_prompt, max_tables=12, include_samples=True)
200
+ targeted = self.connector.inspect_targeted(user_prompt, max_tables=max_tables_limit, include_samples=True)
193
201
  retriever = ContextRetriever(targeted)
194
- return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=12)
202
+ return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=max_tables_limit)
195
203
 
196
204
  def run(
197
205
  self,
@@ -83,15 +83,27 @@ class PromptQueueManager:
83
83
  if not raw_clean:
84
84
  return raw_clean
85
85
 
86
- # Do not split direct SQL statements like "SELECT 1; SELECT 2;"
87
- if raw_clean.lower().startswith(("select ", "insert ", "update ", "delete ", "create ", "alter ", "drop ", "with ")):
88
- return raw_clean
89
-
90
86
  # Do not split special REPL commands
91
87
  if raw_clean.startswith(":"):
92
88
  return raw_clean
93
89
 
94
- # Split by ';' or '&&'
90
+ # Do not split multi-line text with newlines containing SQL statements/keywords
91
+ if "\n" in raw_clean and any(kw in raw_clean.upper() for kw in [
92
+ "SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE", "JOIN", "COMMIT", "BEGIN", "WITH", "CREATE", "ALTER", "DROP"
93
+ ]):
94
+ return raw_clean
95
+
96
+ # Do not split direct SQL statements or code blocks containing SQL DML/DDL
97
+ if re.search(r'\b(?:DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET|SELECT\s+.*?FROM|CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|COMMIT|ROLLBACK|BEGIN)\b', raw_clean, re.IGNORECASE | re.DOTALL):
98
+ return raw_clean
99
+
100
+ # Do not split prompts that ask to review, fix, or correct a code snippet (e.g. "... ; correct this")
101
+ if any(phrase in raw_clean.lower() for phrase in [
102
+ "correct this", "fix this", "check this", "review this", "optimize this", "explain this", "what does this do", "run this", "execute this"
103
+ ]):
104
+ return raw_clean
105
+
106
+ # Split by ';' or '&&' for distinct natural language prompts (e.g. "count users ; show top 5 sites")
95
107
  delimiter_pattern = r'\s*(?:;|&&)\s*'
96
108
  parts = [p.strip() for p in re.split(delimiter_pattern, raw_clean) if p.strip()]
97
109
 
@@ -150,17 +150,23 @@ class SQLValidator:
150
150
 
151
151
  stmt = statements[0]
152
152
 
153
- for pat in cls.READ_PATTERNS:
154
- if re.search(pat, stmt, re.IGNORECASE | re.DOTALL):
155
- return "read"
153
+ # Check for data-modifying CTEs (WITH ... DELETE|INSERT|UPDATE ...)
154
+ if re.match(r"^\s*WITH\b", stmt, re.IGNORECASE):
155
+ if re.search(r"\b(?:DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET)\b", stmt, re.IGNORECASE):
156
+ return "write"
157
+ return "read"
158
+
159
+ for pat in cls.WRITE_PATTERNS:
160
+ if re.search(pat, stmt, re.IGNORECASE):
161
+ return "write"
156
162
 
157
163
  for pat in cls.DDL_PATTERNS:
158
164
  if re.search(pat, stmt, re.IGNORECASE):
159
165
  return "ddl"
160
166
 
161
- for pat in cls.WRITE_PATTERNS:
162
- if re.search(pat, stmt, re.IGNORECASE):
163
- return "write"
167
+ for pat in cls.READ_PATTERNS:
168
+ if re.search(pat, stmt, re.IGNORECASE | re.DOTALL):
169
+ return "read"
164
170
 
165
171
  return "unknown"
166
172
 
@@ -53,8 +53,8 @@ def print_info(message: str) -> None:
53
53
 
54
54
 
55
55
  def print_code(code: str, language: str = "sql", title: Optional[str] = None) -> None:
56
- """Print syntax-highlighted code block."""
57
- syntax = Syntax(code, language, theme="monokai", line_numbers=True)
56
+ """Print syntax-highlighted code block without line numbers for easy copy-pasting."""
57
+ syntax = Syntax(code, language, theme="monokai", line_numbers=False)
58
58
  panel = Panel(syntax, title=f"[bold cyan]{title or language.upper()}[/bold cyan]", border_style="dim")
59
59
  console.print(panel)
60
60
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)
5
5
  Author: Santhosh Gupta
6
6
  License: MIT
@@ -84,6 +84,7 @@ tests/test_graph.py
84
84
  tests/test_knowledge.py
85
85
  tests/test_plugins.py
86
86
  tests/test_prompt_queue.py
87
+ tests/test_prompt_queue_and_sql.py
87
88
  tests/test_providers.py
88
89
  tests/test_recovery.py
89
90
  tests/test_safety.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbagent-cli"
7
- version = "0.11.0"
7
+ version = "0.11.1"
8
8
  description = "The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,48 @@
1
+ """
2
+ Tests for PromptQueueManager SQL script handling and SQLValidator CTE classification.
3
+ """
4
+
5
+ from dbagent.execution.prompt_queue import PromptQueueManager
6
+ from dbagent.sql.validator import SQLValidator
7
+
8
+
9
+ def test_queue_does_not_split_multiline_sql_with_semicolons():
10
+ """Verify that multi-line SQL scripts with internal semicolons are not split into queued fragments."""
11
+ queue_mgr = PromptQueueManager()
12
+
13
+ sql_input = """
14
+ DELETE FROM ivis_customer_user_map WHERE fk_user_id = 5201;
15
+ DELETE FROM ivis_user_access WHERE fk_user_id = 5201;
16
+ DELETE FROM user_activity WHERE user_id = 5201;
17
+ DELETE FROM ivis_user WHERE pk_user_id = 5201;
18
+ COMMIT; correct this
19
+ """
20
+
21
+ res = queue_mgr.split_and_enqueue_chained(sql_input)
22
+ assert res == sql_input.strip()
23
+ assert queue_mgr.has_next() is False
24
+
25
+
26
+ def test_classify_query_with_cte_delete():
27
+ """Verify that data-modifying WITH CTE statements are classified as 'write'."""
28
+ cte_delete = """
29
+ WITH target_user AS (
30
+ SELECT pk_user_id FROM ivis_user WHERE username = 'target_user'
31
+ ),
32
+ del_access AS (
33
+ DELETE FROM ivis_user_access WHERE fk_user_id IN (SELECT pk_user_id FROM target_user)
34
+ )
35
+ DELETE FROM ivis_user WHERE pk_user_id IN (SELECT pk_user_id FROM target_user);
36
+ """
37
+ assert SQLValidator.classify_query(cte_delete) == "write"
38
+
39
+
40
+ def test_classify_query_with_cte_read():
41
+ """Verify that regular SELECT WITH CTE statements remain classified as 'read'."""
42
+ cte_read = """
43
+ WITH active_users AS (
44
+ SELECT pk_user_id, username FROM ivis_user WHERE active = 'Y'
45
+ )
46
+ SELECT * FROM active_users LIMIT 10;
47
+ """
48
+ assert SQLValidator.classify_query(cte_read) == "read"
File without changes
File without changes