Back to home page

EIC code displayed by LXR

 
 

    


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 # the escape character of a prefix match. Not a backslash, which the two databases and the
0030 # Python string between them each read their own way
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     # names of what each row holds, in order
0043     names: tuple[str, ...]
0044     # most rows to keep. The statement asks for one more, which tells the result was cut
0045     limit: int
0046 
0047 
0048 def _identifier(name: str) -> str:
0049     # names come from the allowlist and the dictionary, so this is a second guard, not the first
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     # what to return
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     # conditions
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         # a name the query does not return is a column of the table, which check_query made sure of
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     # each clause starts with a space, which is what keeps a bind variable ending the one before apart from it
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)