def _stringify_scalar(value: ScalarType) -> str: if value is None: return "NULL" elif isinstance(value, bool): return "TRUE" if value else "FALSE" if isinstance(value, (str, bytes)): if isinstance(value, bytes): decoded = value.decode() else: decoded = value decoded = unescaped_quotes.sub("\\'", decoded) decoded = unescaped_newline.sub("\\\\n", decoded) return f"'{decoded}'" elif isinstance(value, (int, float)): return f"{value}" elif isinstance(value, datetime): # Snuba expects naive UTC datetimes, so convert to that if value.tzinfo is not None: delta = value.utcoffset() assert delta is not None value = value - delta value = value.replace(tzinfo=None) return f"toDateTime('{value.isoformat()}')" elif isinstance(value, date): return f"toDateTime('{value.isoformat()}')" elif isinstance(value, list): is_scalar(value) # Throws on an invalid array return f"array({', '.join([_stringify_scalar(v) for v in value])})" elif isinstance(value, tuple): is_scalar(value) # Throws on an invalid tuple return f"tuple({', '.join([_stringify_scalar(v) for v in value])})" raise InvalidExpression(f"'{value}' is not a valid scalar")
def _visit_curried_function(self, func: CurriedFunction) -> str: alias = "" if func.alias is None else f" AS {func.alias}" initialize_clause = "" if func.initializers is not None: initializers = [] for initer in func.initializers: if isinstance(initer, Column): initializers.append(self.visit(initer)) elif isinstance(initer, tuple(Scalar)): initializers.append(_stringify_scalar(initer)) initialize_clause = f"({', '.join(initializers)})" param_clause = "" if func.parameters is not None: params = [] for param in func.parameters: if isinstance(param, (Column, CurriedFunction, Function)): params.append(self.visit(param)) elif is_scalar(param): params.append(_stringify_scalar(param)) param_clause = f"({', '.join(params)})" return f"{func.function}{initialize_clause}{param_clause}{alias}"
def _visit_condition(self, cond: Condition) -> str: rhs = None if cond.is_unary(): rhs = "" elif isinstance(cond.rhs, (Column, CurriedFunction, Function)): rhs = f" {self.visit(cond.rhs)}" elif is_scalar(cond.rhs): rhs = f" {_stringify_scalar(cond.rhs)}" assert rhs is not None return f"{self.visit(cond.lhs)} {cond.op.value}{rhs}"
def _stringify_scalar(self, value: ScalarType) -> str: if value is None: return "NULL" elif isinstance(value, bool): return "TRUE" if value else "FALSE" if isinstance(value, (str, bytes)): if isinstance(value, bytes): decoded = value.decode() else: decoded = value # The ' and \ character are escaped in the string to ensure # the query is valid. They are de-escaped in the SnQL parser. # Also escape newlines since they break the SnQL grammar. decoded = (decoded.replace("\\", "\\\\").replace("'", "\\'").replace( "\n", "\\n")) return f"'{decoded}'" elif isinstance(value, (int, float)): return f"{value}" elif isinstance(value, datetime): # Snuba expects naive UTC datetimes, so convert to that if value.tzinfo is not None: delta = value.utcoffset() assert delta is not None value = value - delta value = value.replace(tzinfo=None) return f"toDateTime('{value.isoformat()}')" elif isinstance(value, date): return f"toDateTime('{value.isoformat()}')" elif isinstance(value, Expression): return self.visit(value) elif isinstance(value, list): is_scalar(value) # Throws on an invalid array return f"array({', '.join([self._stringify_scalar(v) for v in value])})" elif isinstance(value, tuple): is_scalar(value) # Throws on an invalid tuple return f"tuple({', '.join([self._stringify_scalar(v) for v in value])})" raise InvalidExpressionError(f"'{value}' is not a valid scalar")
def validate(self) -> None: if not isinstance(self.function, str): raise InvalidFunctionError( f"function '{self.function}' must be a string") if self.function == "": # TODO: Have a whitelist of valid functions to check, maybe even with more # specific parameter type checking raise InvalidFunctionError("function cannot be empty") if not function_name_re.match(self.function): raise InvalidFunctionError( f"function '{self.function}' contains invalid characters") if self.initializers is not None: if not isinstance(self.initializers, Sequence): raise InvalidFunctionError( f"initializers of function {self.function} must be a Sequence" ) elif not all( isinstance(param, Column) or is_literal(param) for param in self.initializers): raise InvalidFunctionError( f"initializers to function {self.function} must be a scalar or column" ) if self.alias is not None: if not isinstance(self.alias, str) or self.alias == "": raise InvalidFunctionError( f"alias '{self.alias}' of function {self.function} must be None or a non-empty string" ) if not ALIAS_RE.match(self.alias): raise InvalidFunctionError( f"alias '{self.alias}' of function {self.function} contains invalid characters" ) if self.parameters is not None: if not isinstance(self.parameters, Sequence): raise InvalidFunctionError( f"parameters of function {self.function} must be a Sequence" ) for param in self.parameters: if not isinstance( param, (Column, CurriedFunction, Function, Identifier, Lambda)) and not is_scalar(param): assert not isinstance(param, bytes) # mypy raise InvalidFunctionError( f"parameter '{param}' of function {self.function} is an invalid type" )
def validate(self) -> None: if not isinstance(self.lhs, (Column, CurriedFunction, Function)): raise InvalidConditionError( f"invalid condition: LHS of a condition must be a Column, CurriedFunction or Function, not {type(self.lhs)}" ) if not isinstance(self.op, Op): raise InvalidConditionError( "invalid condition: operator of a condition must be an Op") if is_unary(self.op): if self.rhs is not None: raise InvalidConditionError( "invalid condition: unary operators don't have rhs conditions" ) if not isinstance( self.rhs, (Column, CurriedFunction, Function)) and not is_scalar(self.rhs): raise InvalidConditionError( f"invalid condition: RHS of a condition must be a Column, CurriedFunction, Function or Scalar not {type(self.rhs)}" )