Source code for pyexasol.formatter

import re
import string


[docs] class ExaFormatter(string.Formatter): """ :class:`pyexasol.ExaFormatter` is a subclass of :class:`string.Formatter` designed to prevent SQL injections in Exasol dynamic SQL queries. Note: It introduces set of placeholders to prevent SQL injections specifically in Exasol dynamic SQL queries. It also completely disables `format_spec` section of standard formatting since it has no use in context of SQL queries and may cause more harm than good. You may access these functions using `.format` property of connection object. Example: Examples: >>> C = pyexasol.connect(...) ... print(C.format.escape('abc')) """ safe_ident_regexp = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") safe_decimal_regexp = re.compile(r"^(\+|-)?[0-9]+(\.[0-9]+)?$") safe_float_regexp = re.compile(r"^(\+|-)?[0-9]+(\.[0-9]+((e|E)(\+|-)[0-9]+)?)?$") def __init__(self, connection): self.connection = connection self.conversions = { "s": self.quote, "d": self.safe_decimal, "f": self.safe_float, "i": self.safe_ident, "q": self.quote_ident, "r": str, } self.default_conversion = "s" # Set default treatment for identifiers passed as strings to relevant functions if self.connection.options["quote_ident"]: self.default_format_ident = ( self.quote_ident ) # Identifiers will be quoted and escaped self.default_format_ident_value = ( str # Identifier values will be left unchanged ) else: self.default_format_ident = ( self.safe_ident ) # Identifiers will only be checked for safety self.default_format_ident_value = ( str.upper ) # Identifier values will be transformed to upper-case
[docs] def format_field(self, value, format_spec): if format_spec != "": raise ValueError("format_spec is disabled for ExaFormatter") return value
[docs] def convert_field(self, value, conversion): if conversion is None: conversion = self.default_conversion if conversion not in self.conversions: raise ValueError(f"Unknown conversion {conversion}") if isinstance(value, list): if not value: raise ValueError("Trying to format an empty list") return ", ".join([self.conversions[conversion](v) for v in value]) else: return self.conversions[conversion](value)
[docs] @classmethod def escape(cls, val): """ Takes a raw value and converts it into an and escaped string. Args: val: Value to be escaped. Returns: A string where all single quotes ``'`` have been replaced with two single quotes ``''``. """ return str(val).replace("'", "''")
[docs] @classmethod def escape_ident(cls, val): """ Takes a raw value and converts it into an and escaped string. Args: val: Value to be escaped. Returns: A string where all double quotes ``"`` have been replaced with two double quotes ``""``. """ return str(val).replace('"', '""')
[docs] @classmethod def escape_like(cls, val): """ Escape LIKE-patterns. Args: val: Value to be escaped. Returns: A string where all double quotes ``\\`` have been replaced with ``\\\\``, where ``%`` have been replaced with ``\\%``, where ``_`` have been replaced with ``\\_``. """ return ( cls.escape(val) .replace("\\", "\\\\") .replace("%", r"\%") .replace("_", r"\_") )
[docs] @classmethod def quote(cls, val): """ Escapes a string using :meth:`pyexasol.ExaFormatter.escape` and wraps it in single quotes ``'``. """ if val is None: return "NULL" return f"'{cls.escape(val)}'"
[docs] @classmethod def quote_ident(cls, val): """ Escapes an object or a tuple of objects using :meth:`pyexasol.ExaFormatter.escape_ident` and wraps it in double quotes ``"``. Args: val: Raw identifier(s) to be escaped. Returns: str: The formatted and quoted identifier, or joined identifiers if a tuple was provided. """ if isinstance(val, tuple): return ".".join([cls.quote_ident(x) for x in val]) return f'"{cls.escape_ident(val)}"'
[docs] @classmethod def safe_ident(cls, val): """ Convert a raw identifier safely. Args: val (str or tuple): Raw identifier(s). Returns: Validates identifier as string. Raises: ValueError If passed values is not a valid identifier (e.g. contains spaces) Warning: It puts it into SQL query without any quotting. """ if isinstance(val, tuple): return ".".join([cls.safe_ident(x) for x in val]) val = str(val) if not cls.safe_ident_regexp.match(val): if "." in val: parts = val.split(".") raise ValueError( f"Value [{val}] is not a safe identifier. Please use tuple to pass schema names. " f"Example: ('{parts[0]}', '{parts[1]}')" ) elif '"' in val: raise ValueError( f"Value [{val}] is not a safe identifier. Please use 'quote_ident' or '!q' conversion " f"to pass identifiers with lowercase or special characters." ) else: raise ValueError(f"Value [{val}] is not a safe identifier") return val
[docs] @classmethod def safe_float(cls, val): """ Convert a float safely to string. Args: val: Float value to convert. Returns: Validates identifier as string. Raises: ValueError: If value is not valid, e.g.: ``+infinity`` or ``-infinity``. """ if val is None: return "NULL" val = str(val) if not cls.safe_float_regexp.match(val): raise ValueError(f"Value [{val}] is not a safe float") return val
[docs] @classmethod def safe_decimal(cls, val): """ Convert a decimal safely to string. Args: val: Decimal value to convert. Returns: Validates identifier as string. Raises: ValueError: If value is not valid. """ if val is None: return "NULL" val = str(val) if not cls.safe_decimal_regexp.match(val): raise ValueError(f"Value [{val}] is not a safe integer") return val
def __repr__(self): return f"<{self.__class__.__name__} session_id={self.connection.session_id()}>"