This is an automated email from the ASF dual-hosted git repository.

cgivre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/drill-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new 1bef3fb  Check every DROP target under sqlglot 30.20+
1bef3fb is described below

commit 1bef3fb62b611b6cbebaf68aa4561fdf735f434b
Author: cgivre <[email protected]>
AuthorDate: Tue Oct 6 09:34:45 2026 -0400

    Check every DROP target under sqlglot 30.20+
    
    sqlglot 30.20 moved DROP targets into a `tables` list, so the write
    guard could not find the target and rejected permitted DROPs. Read the
    list (falling back to `this`) and check each table against
    writable_plugins.
---
 drill_mcp/guard.py  | 40 +++++++++++++++++++++-------------------
 tests/test_guard.py |  6 +++++-
 2 files changed, 26 insertions(+), 20 deletions(-)

diff --git a/drill_mcp/guard.py b/drill_mcp/guard.py
index a20c32a..a5919d5 100644
--- a/drill_mcp/guard.py
+++ b/drill_mcp/guard.py
@@ -212,23 +212,24 @@ def _check_write(statement: exp.Expression, policy: 
Policy, depth: int) -> None:
             raise PolicyError(
                 f"{statement.key.upper()} {kind or 'UNKNOWN'} is not permitted"
             )
-        target = _write_target(statement)
-        if target is None:
+        targets = _write_targets(statement)
+        if not targets:
             raise PolicyError(
                 "could not determine the target of this statement, rejecting"
             )
-        qualified = _schema_prefix(target)
-        if not qualified:
-            raise PolicyError(
-                f"write target '{target.name}' is not schema-qualified; "
-                "qualify it with a plugin listed in writable_plugins"
-            )
-        if not matches_prefix(qualified, policy.writable_plugins):
-            raise PolicyError(
-                f"writes to '{qualified}' are not permitted; "
-                f"add it to writable_plugins to allow this (currently: "
-                f"{list(policy.writable_plugins) or 'none'})"
-            )
+        for target in targets:
+            qualified = _schema_prefix(target)
+            if not qualified:
+                raise PolicyError(
+                    f"write target '{target.name}' is not schema-qualified; "
+                    "qualify it with a plugin listed in writable_plugins"
+                )
+            if not matches_prefix(qualified, policy.writable_plugins):
+                raise PolicyError(
+                    f"writes to '{qualified}' are not permitted; "
+                    f"add it to writable_plugins to allow this (currently: "
+                    f"{list(policy.writable_plugins) or 'none'})"
+                )
         return
 
     raise PolicyError(f"statement type {statement.key.upper()} is not 
permitted")
@@ -250,11 +251,12 @@ def _explain_body(statement: exp.Command) -> str:
     return re.sub(r"^\s*PLAN\s+FOR\s+", "", text, flags=re.IGNORECASE)
 
 
-def _write_target(statement: exp.Expression) -> exp.Table | None:
-    target = statement.this
-    if isinstance(target, exp.Schema):
-        target = target.this
-    return target if isinstance(target, exp.Table) else None
+def _write_targets(statement: exp.Expression) -> list[exp.Table]:
+    # sqlglot >= 30.20 puts DROP targets in a `tables` list (DROP TABLE a, b);
+    # older releases and CREATE use `this`. Any non-Table target fails closed.
+    targets = statement.args.get("tables") or [statement.this]
+    targets = [t.this if isinstance(t, exp.Schema) else t for t in targets]
+    return targets if all(isinstance(t, exp.Table) for t in targets) else []
 
 
 def _schema_prefix(table: exp.Table) -> str:
diff --git a/tests/test_guard.py b/tests/test_guard.py
index 36795ef..3b78d21 100644
--- a/tests/test_guard.py
+++ b/tests/test_guard.py
@@ -124,6 +124,10 @@ class TestWritesWithAnAllowlist:
     def test_drop_in_allowed_plugin_permitted(self):
         check("DROP TABLE dfs.tmp.old", OPEN)
 
+    def test_multi_table_drop_checks_every_target(self):
+        with pytest.raises(PolicyError):
+            check("DROP TABLE dfs.tmp.old, s3.bucket.prod", OPEN)
+
     def test_ctas_into_other_plugin_rejected(self):
         with pytest.raises(PolicyError, match="writable_plugins"):
             check("CREATE TABLE s3.bucket.out AS SELECT 1", OPEN)
@@ -440,7 +444,7 @@ class TestCoverageGaps:
 
     def test_ctas_with_explicit_column_list_permitted(self):
         # The parenthesized column list wraps the target table in an
-        # exp.Schema node; _write_target must unwrap it to find the table.
+        # exp.Schema node; _write_targets must unwrap it to find the table.
         check("CREATE TABLE dfs.tmp.out (a INT) AS SELECT 1", OPEN)
 
     def test_create_with_no_resolvable_target_rejected(self):

Reply via email to