Files
superset2/tests/unit_tests/models/test_helpers_offset.py

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)."
)