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