mapledan commented on code in PR #42785:
URL: https://github.com/apache/superset/pull/42785#discussion_r3963632031


##########
superset/commands/sql_lab/estimate.py:
##########
@@ -148,9 +149,20 @@ def run(
     ) -> list[dict[str, Any]]:
         self.validate()
 
+        template_processor = get_template_processor(self._database)
+        # A templated query has no single execution plan: it expands using
+        # values only available at run time (a dashboard's time range, the
+        # current user, a URL parameter), and different expansions can produce
+        # different plans. Estimating one of them -- here, the emptiest one,
+        # with no such context to expand from -- would report the plan of a
+        # different query than the one that runs. Refusing before the SQL
+        # reaches `SQLScript` also replaces the parse error the raw `{%` would
+        # otherwise trigger, which reads as a typo in a valid query.
+        if template_processor.has_template(self._sql):

Review Comment:
   You're right, and it made me re-check the premise the whole thing rested on.
   
   The estimate endpoint is SQL Lab-only (QuerySource.SQL_LAB), and SQL Lab has 
no dashboard context either — so a context macro renders the same on the 
estimate path as it does on Run. Refusing was wrong both for the bound case you 
describe and for the unbound one I was worried about.
   
   Nothing is refused now. The gate on template_params is gone, so {{ ds }} 
with {"ds": ...} renders and estimates exactly as before. That also turns out 
to fix the case I opened the PR for, on its own — a query using only 
get_time_filter() declares no parameter, so the gate never rendered it and the 
raw {% reached SQLScript().
   
   Worth noting validate() right above already jinja-processes through 
process_jinja_sql without consulting template_params, so the two halves of this 
command now agree.



##########
superset/jinja_context.py:
##########
@@ -797,6 +797,36 @@ def get_context(self) -> dict[str, Any]:
         """
         return self._context.copy()
 
+    def has_template(self, sql: str) -> bool:
+        """Whether the SQL contains anything for ``process_template`` to expand
+
+        Lexed rather than parsed, so that a comment -- which leaves no trace in
+        a parsed template -- still counts, and using this processor's own
+        environment, so that any customized delimiters are honored. Lexing
+        evaluates nothing.
+
+        >>> has_template("SELECT '{{ current_username() }}'")
+        True
+        >>> has_template("{{ dataset(1) }}")
+        True
+        >>> has_template("SELECT '{{1,2},{3,4}}'::int[]")
+        False
+        """
+        try:
+            # The whole stream is consumed before deciding, rather than 
stopping
+            # at the first non-`data` token: `'{{1,2},{3,4}}'` opens like a
+            # template and only turns out not to be one further along, where 
the
+            # lexer gives up.
+            kinds = {kind for _, kind, _ in self.env.lex(sql)}
+        except TemplateSyntaxError:

Review Comment:
   Confirmed — your SQL reproduces it.
   
   I tried the direction you suggested first, and keeping the tokens seen 
before the
   error flips the array literal on its own to `True`: `{{1,2}` emits 
`variable_begin`
   before the lexer gives up, so "saw a template token" cannot separate the two 
cases.
   
   What does separate them is requiring the construct to have *closed*. The 
literal is
   abandoned unterminated, so it never emits `variable_end`:
   
   | SQL | want | seen-tokens | closed-only |
   |---|---|---|---|
   | `SELECT '{{1,2},{3,4}}'::int[]` | False | True ❌ | False ✅ |
   | `SELECT '{{ current_username() }}', '{{1,2},{3,4}}'::int[]` | True | True 
✅ | True ✅ |
   
   Changed to that, with your example as a test case.
   
   Also worth saying that the stakes here dropped along the way: detection now 
only
   picks the wording of a parse error that is raised either way, so getting it 
wrong
   costs the better message and nothing else.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to