| # 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. | |
| from typing import Any | |
| from pypaimon.common.predicate import Predicate | |
| _BINARY_OPS = { | |
| 'equal': '=', | |
| 'notEqual': '!=', | |
| 'lessThan': '<', | |
| 'lessOrEqual': '<=', | |
| 'greaterThan': '>', | |
| 'greaterOrEqual': '>=', | |
| } | |
| def render_predicate(predicate: Predicate) -> str: | |
| """Render a :class:`Predicate` tree as a human-readable string. | |
| The renderer relies only on the existing ``method`` / ``field`` / | |
| ``literals`` shape and never mutates the predicate. Used by | |
| ``ReadBuilder.explain()`` and intentionally lives outside | |
| :mod:`pypaimon.common.predicate` so the predicate module stays | |
| rendering-agnostic. | |
| """ | |
| if predicate is None: | |
| return "" | |
| method = predicate.method | |
| field = predicate.field | |
| literals = predicate.literals | |
| if method == 'and': | |
| return _join_children(literals, 'AND') | |
| if method == 'or': | |
| return _join_children(literals, 'OR') | |
| if method in _BINARY_OPS: | |
| return "{} {} {}".format(field, _BINARY_OPS[method], _format_literal(literals[0])) | |
| if method == 'in': | |
| return "{} IN [{}]".format(field, ", ".join(_format_literal(v) for v in literals)) | |
| if method == 'notIn': | |
| return "{} NOT IN [{}]".format(field, ", ".join(_format_literal(v) for v in literals)) | |
| if method == 'between': | |
| return "{} BETWEEN {} AND {}".format( | |
| field, _format_literal(literals[0]), _format_literal(literals[1])) | |
| if method == 'notBetween': | |
| return "{} NOT BETWEEN {} AND {}".format( | |
| field, _format_literal(literals[0]), _format_literal(literals[1])) | |
| if method == 'isNull': | |
| return "{} IS NULL".format(field) | |
| if method == 'isNotNull': | |
| return "{} IS NOT NULL".format(field) | |
| if method == 'startsWith': | |
| return "{} STARTSWITH {}".format(field, _format_literal(literals[0])) | |
| if method == 'endsWith': | |
| return "{} ENDSWITH {}".format(field, _format_literal(literals[0])) | |
| if method == 'contains': | |
| return "{} CONTAINS {}".format(field, _format_literal(literals[0])) | |
| if method == 'like': | |
| return "{} LIKE {}".format(field, _format_literal(literals[0])) | |
| return "{}({}{})".format( | |
| method, | |
| field if field is not None else "", | |
| ", " + ", ".join(_format_literal(v) for v in (literals or [])) if literals else "", | |
| ) | |
| def _join_children(children, joiner: str) -> str: | |
| parts = [render_predicate(c) for c in (children or [])] | |
| parts = [p for p in parts if p] | |
| if not parts: | |
| return "" | |
| if len(parts) == 1: | |
| return parts[0] | |
| return " {} ".format(joiner).join("({})".format(p) for p in parts) | |
| def _format_literal(value: Any) -> str: | |
| if value is None: | |
| return "NULL" | |
| if isinstance(value, str): | |
| return "'{}'".format(value.replace("'", "\\'")) | |
| if isinstance(value, bytes): | |
| return "b'{}'".format(value.hex()) | |
| return repr(value) |