mirror of
https://github.com/apache/superset.git
synced 2026-07-25 16:12:39 +00:00
126 lines
4.8 KiB
Python
126 lines
4.8 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 ast
|
|
from pathlib import Path
|
|
|
|
HELPERS_PATH = (
|
|
Path(__file__).resolve().parents[3] / "superset" / "models" / "helpers.py"
|
|
)
|
|
|
|
|
|
def _uses_supports_offset(node: ast.AST, *, negated: bool = False) -> bool:
|
|
"""
|
|
True if `node` contains a positive (non-negated) reference to
|
|
`supports_offset` — i.e. a guard that reads true when the engine
|
|
*does* support OFFSET. A `not X.supports_offset` or
|
|
`X.supports_offset == False` reference does not count: gating
|
|
`.offset()` on an inverted condition would emit OFFSET on the exact
|
|
engines this guard exists to protect against.
|
|
"""
|
|
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
return _uses_supports_offset(node.operand, negated=not negated)
|
|
|
|
if isinstance(node, ast.Attribute) and node.attr == "supports_offset":
|
|
return not negated
|
|
|
|
if isinstance(node, ast.BoolOp):
|
|
return any(
|
|
_uses_supports_offset(value, negated=negated) for value in node.values
|
|
)
|
|
|
|
if isinstance(node, ast.Compare):
|
|
operands = [node.left, *node.comparators]
|
|
if not any(
|
|
isinstance(operand, ast.Attribute) and operand.attr == "supports_offset"
|
|
for operand in operands
|
|
):
|
|
return False
|
|
compares_false = any(
|
|
isinstance(operand, ast.Constant) and operand.value is False
|
|
for operand in operands
|
|
)
|
|
inverting_op = any(isinstance(op, (ast.Eq, ast.Is)) for op in node.ops)
|
|
return compares_false != inverting_op and not negated
|
|
|
|
return any(
|
|
_uses_supports_offset(child, negated=negated)
|
|
for child in ast.iter_child_nodes(node)
|
|
)
|
|
|
|
|
|
def _is_qry_offset_assignment(stmt: ast.AST) -> bool:
|
|
"""True if stmt is `qry = qry.offset(...)` (any LHS, call to `.offset`)."""
|
|
if not isinstance(stmt, ast.Assign):
|
|
return False
|
|
call = stmt.value
|
|
if not isinstance(call, ast.Call):
|
|
return False
|
|
func = call.func
|
|
return isinstance(func, ast.Attribute) and func.attr == "offset"
|
|
|
|
|
|
def test_helpers_guards_offset_with_supports_offset_flag() -> None:
|
|
"""
|
|
Regression guard: the `.offset()` call in get_sqla_query must be wrapped
|
|
in an `if` that checks `supports_offset`. Without this guard,
|
|
engines that do not support OFFSET (Elasticsearch SQL) crash drill-
|
|
to-detail on page 2+.
|
|
|
|
We parse the AST rather than grep the source so the test survives
|
|
Black-style reformatting and trivial refactors.
|
|
"""
|
|
source = HELPERS_PATH.read_text()
|
|
assert "supports_offset" in source, (
|
|
"helpers.py no longer references supports_offset; the OFFSET "
|
|
"guard is gone — Elasticsearch drill-to-detail will crash on page 2+."
|
|
)
|
|
|
|
tree = ast.parse(source)
|
|
unguarded: list[int] = []
|
|
|
|
class Visitor(ast.NodeVisitor):
|
|
"""Flag `.offset()` assignments not guarded by a `supports_offset` check."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Track nesting depth inside `supports_offset`-guarded `if` blocks."""
|
|
self._in_guarded_if = 0
|
|
|
|
def visit_If(self, node: ast.If) -> None: # noqa: N802
|
|
"""Descend into the body under a `supports_offset` guard when present."""
|
|
if _uses_supports_offset(node.test):
|
|
self._in_guarded_if += 1
|
|
for child in node.body:
|
|
self.visit(child)
|
|
self._in_guarded_if -= 1
|
|
for child in node.orelse:
|
|
self.visit(child)
|
|
else:
|
|
self.generic_visit(node)
|
|
|
|
def visit_Assign(self, node: ast.Assign) -> None: # noqa: N802
|
|
"""Record any `qry = qry.offset(...)` seen outside a guard."""
|
|
if _is_qry_offset_assignment(node) and self._in_guarded_if == 0:
|
|
unguarded.append(node.lineno)
|
|
self.generic_visit(node)
|
|
|
|
Visitor().visit(tree)
|
|
assert not unguarded, (
|
|
f"Unguarded .offset() call(s) in helpers.py at line(s) {unguarded}. "
|
|
"Wrap with `if ... supports_offset:` to prevent OFFSET emission "
|
|
"on engines that cannot parse it (e.g. Elasticsearch SQL)."
|
|
)
|