pavansai1997 commented on code in PR #76230:
URL: https://github.com/apache/airflow/pull/76230#discussion_r4436232040


##########
providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py:
##########
@@ -80,13 +83,21 @@ class LLMSQLQueryOperator(LLMOperator):
     :param db_conn_id: Connection ID for database schema introspection.
         The connection must resolve to a ``DbApiHook``.
     :param table_names: Tables to include in the LLM's schema context.
-        Used with ``db_conn_id`` for automatic introspection.
+        Used with ``db_conn_id`` for automatic introspection. This does not 
restrict
+        the tables the generated SQL may reference; use ``allowed_tables`` for 
that.
     :param schema_context: Manual schema context string. When provided,
         this is used instead of ``db_conn_id`` introspection.
     :param validate_sql: Whether to validate generated SQL via AST parsing.
         Default ``True`` (safe by default).
     :param allowed_sql_types: SQL statement types to allow.
         Default: ``(Select, Union, Intersect, Except)``.
+    :param allowed_tables: Tables the generated SQL may reference, as 
``table`` or

Review Comment:
   Good point — a schema is a natural unit to allow-list (e.g. a reporting 
schema whose tables change over
   time). Since the matching now lives in one shared helper, I'd suggest 
supporting schema.* entries in the
   same allowed_tables list, e.g. allowed_tables=["analytics.*", 
"public.dim_date"], rather than adding a
   separate parameter. It would apply to both LLMSQLQueryOperator and 
SQLToolset so their rules stay
   identical. To stay fail-closed, an unqualified reference wouldn't match a 
schema entry unless a default
   schema is known (SQLToolset(schema=...)), since the operator can't see the 
database's search path, and a
   bare * would be rejected in favour of None. Happy to add this to this PR if 
that works for you.
   
   ---
   Drafted-by: Code Puppy; reviewed by @pavansai1997 before posting



##########
providers/common/ai/tests/unit/common/ai/operators/test_llm_sql.py:
##########
@@ -759,6 +759,103 @@ def _make_context(ti_id=None):
 @pytest.mark.skipif(
     not AIRFLOW_V_3_1_PLUS, reason="Human in the loop is only compatible with 
Airflow >= 3.1.0"
 )
+class TestLLMSQLQueryOperatorAllowedTables:

Review Comment:
   Good catch, my mistake. Moved the class above the decorator so the approval 
suite keeps its 3.1 skip, and `test_reviewer_modified_sql_is_checked` now has 
its own skip, in 80e91e559b.
   
   ---
   Drafted-by: Code Puppy (no human review before posting)



##########
providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py:
##########
@@ -192,10 +216,27 @@ def execute_complete(
         """Resume after human review, re-validating if the reviewer modified 
the SQL."""
         output = self._complete_review(context, generated_output, event, 
decision)
         if output != generated_output:
-            _validate_sql(output, allowed_types=self.allowed_sql_types, 
dialect=self._resolved_dialect)
+            self._check_sql(output)
         self._clear_usage_budget(context)
         return output
 
+    def _check_sql(self, sql: str) -> None:
+        """Validate the statement types of ``sql`` and, when set, the tables 
it may reference."""
+        statements = _validate_sql(sql, allowed_types=self.allowed_sql_types, 
dialect=self._resolved_dialect)
+        if self.allowed_tables is None:
+            return
+        violations = find_allowed_tables_violations(statements, 
canonical_allowed_tables(self.allowed_tables))

Review Comment:
   Added `allowed_functions` with the same semantics as `SQLToolset` 
(case-insensitive, only consulted when `allowed_tables` is set), so the 
existing message is now accurate. 
`test_allowed_functions_permits_unrecognized_functions` covers your 
`json_build_object` and `age` examples, and `age` without it is pinned as 
refused.
   
   ---
   Drafted-by: Code Puppy (no human review before posting)



##########
providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py:
##########
@@ -172,7 +196,7 @@ def execute(self, context: Context) -> str:
         sql = self._strip_llm_output(result.output, 
dialect=self._resolved_dialect)
 
         if self.validate_sql:
-            _validate_sql(sql, allowed_types=self.allowed_sql_types, 
dialect=self._resolved_dialect)
+            self._check_sql(sql)

Review Comment:
   Done both. With `allowed_tables` set, the prompt lists the tables and asks 
for unquoted table names and no comments, and the check runs as an output 
validator that raises `ModelRetry`, so `output_retries` covers it. The post-run 
check stays as a backstop and the reviewer path still raises `SQLSafetyError`. 
`test_refused_sql_is_sent_back_to_the_model` drives a real `Agent` with a 
`FunctionModel` through a refusal and a successful retry.
   
   ---
   Drafted-by: Code Puppy (no human review before posting)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to