Warning, file /panda-server/pandaserver/asyncprocess/db_read/compose.py was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 """
0002 SQL of the statements of an async db_read request.
0003
0004 Each table of a query gets a statement of its own. The names in it come from the allowlist
0005 and the data dictionary, never from the text of the request, and every value is a bind
0006 variable. A statement asks for one row more than the limit of its query, so that a result
0007 cut at the limit says so: the limit is written in the statement because with Postgres the
0008 driver fetches the whole result when it executes, which a fetch of fewer rows does not stop.
0009
0010 The text keeps clear of what WrappedCursor rewrites for Postgres: there is no ":" or "%"
0011 outside the bind variables, and each bind variable is followed by a space, a comma or a
0012 parenthesis.
0013 """
0014
0015 import re
0016 from dataclasses import dataclass
0017 from typing import Any
0018
0019 from pandaserver.asyncprocess.db_read.request_shape import (
0020 NULL_OPS,
0021 Aggregate,
0022 FromQuery,
0023 GroupKey,
0024 Query,
0025 TablePlan,
0026 )
0027 from pandaserver.asyncprocess.db_read.schema_info import KIND_LOB, STATEMENT_COMMENT
0028
0029
0030
0031 _LIKE_ESCAPE = "!"
0032
0033 _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$#]*$")
0034
0035 _COMPARISONS = {"=": "=", "!=": "<>", "<": "<", "<=": "<=", ">": ">", ">=": ">="}
0036
0037
0038 @dataclass(frozen=True)
0039 class Statement:
0040 sql: str
0041 binds: dict[str, Any]
0042
0043 names: tuple[str, ...]
0044
0045 limit: int
0046
0047
0048 def _identifier(name: str) -> str:
0049
0050 if not _IDENTIFIER.match(name):
0051 raise ValueError(f"{name!r} is not a plain identifier")
0052 return name
0053
0054
0055 def escape_prefix(prefix: str) -> str:
0056 """
0057 Get the LIKE pattern that matches a text starting with the prefix, taken literally.
0058
0059 Args:
0060 prefix(str): the start of the text
0061
0062 Returns:
0063 str: the pattern, to be used with ESCAPE '!'
0064 """
0065 escaped = prefix.replace(_LIKE_ESCAPE, _LIKE_ESCAPE * 2).replace("%", f"{_LIKE_ESCAPE}%").replace("_", f"{_LIKE_ESCAPE}_")
0066 return escaped + "%"
0067
0068
0069 def _group_expression(key: GroupKey, backend: str) -> str:
0070 column = _identifier(key.column)
0071 if key.truncation is None:
0072 return column
0073 if backend == "postgres":
0074 return f"date_trunc('{key.truncation}',{column})"
0075 return f"TRUNC({column},'{'DD' if key.truncation == 'day' else 'HH24'}')"
0076
0077
0078 def _aggregate_expression(aggregate: Aggregate) -> str:
0079 argument = "*" if aggregate.column is None else _identifier(aggregate.column)
0080 return f"{aggregate.function.upper()}({argument})"
0081
0082
0083 def compose_statement(plan: TablePlan, query: Query, from_values: dict[FromQuery, list[Any]], backend: str) -> Statement | None:
0084 """
0085 Compose the statement that reads one table of a query.
0086
0087 Args:
0088 plan(TablePlan): the table, from check_query
0089 query(Query): the query
0090 from_values(dict): the values of each "in" from a query
0091 backend(str): oracle or postgres
0092
0093 Returns:
0094 Statement or None: the statement, or None when an "in" from a query has no values, which no
0095 row can satisfy
0096 """
0097 binds: dict[str, Any] = {}
0098
0099 def bind(value: Any) -> str:
0100 name = f":r{len(binds)}"
0101 binds[name] = value
0102 return name
0103
0104
0105 expressions: list[str] = []
0106 names: list[str] = []
0107 if query.grouped:
0108 for key in query.group_by:
0109 expressions.append(_group_expression(key, backend))
0110 names.append(key.name)
0111 for aggregate in query.aggregates:
0112 expressions.append(_aggregate_expression(aggregate))
0113 names.append(aggregate.name)
0114 else:
0115 columns = query.columns or tuple(column for column, kind in plan.info.columns.items() if kind != KIND_LOB)
0116 expressions = [_identifier(column) for column in columns]
0117 names = list(columns)
0118 expression_of = dict(zip(names, expressions))
0119
0120 conditions: list[str] = []
0121 for condition in plan.where:
0122 column = _identifier(condition.column)
0123 if condition.op in NULL_OPS:
0124 conditions.append(f"{column} {condition.op.upper()}")
0125 elif condition.op == "in":
0126 values = from_values[condition.value] if isinstance(condition.value, FromQuery) else condition.value
0127 if not values:
0128 return None
0129 conditions.append(f"{column} IN ({','.join(bind(value) for value in values)})")
0130 elif condition.op == "prefix":
0131 conditions.append(f"{column} LIKE {bind(escape_prefix(condition.value))} ESCAPE '{_LIKE_ESCAPE}'")
0132 else:
0133 conditions.append(f"{column} {_COMPARISONS[condition.op]} {bind(condition.value)}")
0134 sql = f"SELECT {','.join(expressions)} FROM {_identifier(plan.info.owner)}.{_identifier(plan.info.table_name)}"
0135 if conditions:
0136 sql += " WHERE " + " AND ".join(conditions)
0137 if query.group_by:
0138 sql += " GROUP BY " + ",".join(_group_expression(key, backend) for key in query.group_by)
0139 if query.order_by:
0140
0141 order = [f"{expression_of.get(key.name) or _identifier(key.name)}{' DESC' if key.descending else ''}" for key in query.order_by]
0142 sql += " ORDER BY " + ",".join(order)
0143
0144 if backend == "postgres":
0145 sql += f" LIMIT {query.limit + 1}"
0146 else:
0147 sql += f" FETCH FIRST {query.limit + 1} ROWS ONLY"
0148 return Statement(sql + STATEMENT_COMMENT, binds, tuple(names), query.limit)