mirror of
https://github.com/apache/superset.git
synced 2026-07-19 05:05:39 +00:00
sqlglot cannot parse Trino SQL routine syntax, so queries declaring inline UDFs failed to parse in SQL Lab: the parser splits statements on every semicolon (including the ones inside BEGIN ... END routine bodies) and has no grammar for FUNCTION specifications in a WITH clause. The upstream issue (tobymao/sqlglot#5178) was closed as low priority, so this extends the Trino dialect on the Superset side. The custom dialect keeps routine bodies intact when splitting statements and parses inline function specifications into opaque InlineUDF nodes that regenerate verbatim. Trino does not allow queries inside SQL UDF bodies, so the opaque representation hides no table references from Superset's security checks. The extensions only activate on syntax that fails to parse today, so existing queries are unaffected. Fixes #26162 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
240 lines
6.6 KiB
Python
240 lines
6.6 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import pytest
|
|
import sqlglot
|
|
|
|
from superset.exceptions import SupersetParseError
|
|
from superset.sql.dialects.trino import InlineUDF, Trino
|
|
from superset.sql.parse import SQLScript, SQLStatement, Table
|
|
|
|
# example from https://trino.io/docs/current/udf/sql/begin.html, reported in
|
|
# https://github.com/apache/superset/issues/26162
|
|
INLINE_UDF = """
|
|
WITH FUNCTION meaning_of_life()
|
|
RETURNS tinyint
|
|
BEGIN
|
|
DECLARE a tinyint DEFAULT CAST(6 AS tinyint);
|
|
DECLARE b tinyint DEFAULT CAST(7 AS tinyint);
|
|
RETURN a * b;
|
|
END
|
|
SELECT meaning_of_life()
|
|
""".strip()
|
|
|
|
|
|
def test_inline_udf_is_single_statement() -> None:
|
|
"""
|
|
Semicolons inside the routine body must not split the statement.
|
|
"""
|
|
statements = sqlglot.parse(INLINE_UDF, dialect=Trino)
|
|
assert len(statements) == 1
|
|
assert len(list(statements[0].find_all(InlineUDF))) == 1
|
|
|
|
|
|
def test_inline_udf_generates_verbatim() -> None:
|
|
"""
|
|
The function specification should be preserved verbatim, and the
|
|
generated SQL should be parseable again.
|
|
"""
|
|
statement = sqlglot.parse_one(INLINE_UDF, dialect=Trino)
|
|
generated = statement.sql(dialect=Trino)
|
|
assert (
|
|
"""
|
|
WITH FUNCTION meaning_of_life()
|
|
RETURNS tinyint
|
|
BEGIN
|
|
DECLARE a tinyint DEFAULT CAST(6 AS tinyint);
|
|
DECLARE b tinyint DEFAULT CAST(7 AS tinyint);
|
|
RETURN a * b;
|
|
END
|
|
""".strip()
|
|
in generated
|
|
)
|
|
assert sqlglot.parse_one(generated, dialect=Trino)
|
|
|
|
|
|
def test_inline_udf_return_form() -> None:
|
|
"""
|
|
Test functions whose body is a single ``RETURN`` expression, including
|
|
multiple comma-separated functions in one ``WITH`` clause.
|
|
"""
|
|
sql = """
|
|
WITH
|
|
FUNCTION hello(name varchar)
|
|
RETURNS varchar
|
|
RETURN format('Hello %s!', name),
|
|
FUNCTION bye()
|
|
RETURNS varchar
|
|
RETURN 'Bye!'
|
|
SELECT hello('Finn') || ' and ' || bye()
|
|
""".strip()
|
|
statement = sqlglot.parse_one(sql, dialect=Trino)
|
|
assert len(list(statement.find_all(InlineUDF))) == 2
|
|
|
|
generated = statement.sql(dialect=Trino)
|
|
assert "RETURN format('Hello %s!', name)" in generated
|
|
assert "RETURN 'Bye!'" in generated
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"sql",
|
|
[
|
|
"""
|
|
WITH FUNCTION classify(a bigint)
|
|
RETURNS varchar
|
|
BEGIN
|
|
CASE a
|
|
WHEN 0 THEN RETURN 'zero';
|
|
WHEN 1 THEN RETURN 'one';
|
|
ELSE RETURN 'more than one or negative';
|
|
END CASE;
|
|
RETURN NULL;
|
|
END
|
|
SELECT classify(x) FROM some_table
|
|
""",
|
|
"""
|
|
WITH FUNCTION classify(a bigint)
|
|
RETURNS varchar
|
|
BEGIN
|
|
IF a > 100 THEN
|
|
RETURN 'big';
|
|
ELSEIF a > 0 THEN
|
|
RETURN 'small';
|
|
END IF;
|
|
RETURN 'negative';
|
|
END
|
|
SELECT classify(x) FROM some_table
|
|
""",
|
|
"""
|
|
WITH FUNCTION classify(a bigint)
|
|
RETURNS varchar
|
|
BEGIN
|
|
WHILE a < 100 DO
|
|
SET a = a + 1;
|
|
END WHILE;
|
|
RETURN IF(a = 100, 'hundred', 'other');
|
|
END
|
|
SELECT classify(x) FROM some_table
|
|
""",
|
|
],
|
|
)
|
|
def test_inline_udf_nested_blocks(sql: str) -> None:
|
|
"""
|
|
Test nested blocks: ``CASE ... END CASE``, ``IF ... END IF``,
|
|
``WHILE ... END WHILE``, and scalar ``IF()`` function calls.
|
|
"""
|
|
statements = sqlglot.parse(sql.strip(), dialect=Trino)
|
|
assert len(statements) == 1
|
|
|
|
|
|
def test_cte_named_function_still_works() -> None:
|
|
"""
|
|
A CTE named "function" must still be parsed as a regular CTE.
|
|
"""
|
|
sql = "WITH function AS (SELECT 1 AS x) SELECT x FROM function"
|
|
statement = sqlglot.parse_one(sql, dialect=Trino)
|
|
assert not list(statement.find_all(InlineUDF))
|
|
assert statement.sql(dialect=Trino) == sql
|
|
|
|
|
|
def test_unbalanced_body_raises() -> None:
|
|
"""
|
|
An unterminated routine body should raise a parse error.
|
|
"""
|
|
sql = "WITH FUNCTION f() RETURNS int BEGIN RETURN 1; SELECT f()"
|
|
with pytest.raises(sqlglot.errors.ParseError):
|
|
sqlglot.parse(sql, dialect=Trino)
|
|
|
|
|
|
def test_missing_body_raises() -> None:
|
|
"""
|
|
A function specification without a body should raise a parse error.
|
|
"""
|
|
sql = "WITH FUNCTION f() RETURNS int SELECT f()"
|
|
with pytest.raises(sqlglot.errors.ParseError):
|
|
sqlglot.parse(sql, dialect=Trino)
|
|
|
|
|
|
def test_sqlscript_inline_udf() -> None:
|
|
"""
|
|
Integration with the Superset parsing API (reproduces #26162).
|
|
"""
|
|
script = SQLScript(INLINE_UDF, "trino")
|
|
assert len(script.statements) == 1
|
|
assert not script.has_mutation()
|
|
|
|
statement = script.statements[0]
|
|
assert statement.is_select()
|
|
assert statement.format() == statement.format() # deterministic
|
|
|
|
|
|
def test_sqlscript_inline_udf_multiple_statements() -> None:
|
|
"""
|
|
Statements after the UDF query should still be split correctly.
|
|
"""
|
|
script = SQLScript(f"{INLINE_UDF};\nSELECT 42", "trino")
|
|
assert len(script.statements) == 2
|
|
|
|
|
|
def test_sqlstatement_extract_tables() -> None:
|
|
"""
|
|
Tables referenced by the main query should still be extracted.
|
|
"""
|
|
sql = """
|
|
WITH FUNCTION doubleup(x integer)
|
|
RETURNS integer
|
|
BEGIN
|
|
RETURN x * 2;
|
|
END
|
|
SELECT doubleup(some_column) FROM some_table
|
|
""".strip()
|
|
statement = SQLStatement(sql, "trino")
|
|
assert statement.tables == {Table("some_table")}
|
|
|
|
|
|
def test_sqlstatement_regular_queries_unaffected() -> None:
|
|
"""
|
|
Regular Trino queries should parse exactly as before.
|
|
"""
|
|
script = SQLScript(
|
|
"WITH t AS (SELECT 1 AS x) SELECT * FROM t; SELECT 2",
|
|
"trino",
|
|
)
|
|
assert len(script.statements) == 2
|
|
assert script.statements[0].tables == set()
|
|
|
|
with pytest.raises(SupersetParseError):
|
|
SQLStatement("SELECT * FROM", "trino")
|
|
|
|
|
|
def test_create_function_not_split() -> None:
|
|
"""
|
|
``CREATE FUNCTION`` bodies should not be split on semicolons either.
|
|
"""
|
|
sql = """
|
|
CREATE FUNCTION meaning_of_life()
|
|
RETURNS tinyint
|
|
BEGIN
|
|
DECLARE a tinyint DEFAULT CAST(6 AS tinyint);
|
|
DECLARE b tinyint DEFAULT CAST(7 AS tinyint);
|
|
RETURN a * b;
|
|
END;
|
|
SELECT 42
|
|
""".strip()
|
|
statements = sqlglot.parse(sql, dialect=Trino)
|
|
assert len(statements) == 2
|