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.
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/PKG-INFO +1 -1
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/generator.py +4 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/pipeline.py +16 -8
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/prompt_queue.py +17 -5
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/validator.py +12 -6
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/console.py +2 -2
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/SOURCES.txt +1 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/pyproject.toml +1 -1
- dbagent_cli-0.11.1/tests/test_prompt_queue_and_sql.py +48 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/README.md +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/benchmark.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/cli.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/config.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/errors.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/plugins.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sample_db.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/ground_truth.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/chat_screen.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/live_streamer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/setup.cfg +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_benchmarks.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_cli.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_demo_and_sample_db.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_formats_and_exit_codes.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_generator.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_graph.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_plugins.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_prompt_queue.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_providers.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_safety.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_schema.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_security.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.1}/tests/test_semantic.py +0 -0
|
@@ -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
|
|
180
|
-
if
|
|
181
|
-
target_names
|
|
182
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
#
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
162
|
-
if re.search(pat, stmt, re.IGNORECASE):
|
|
163
|
-
return "
|
|
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=
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.11.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|