-
Notifications
You must be signed in to change notification settings - Fork 304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
⭐ Improve parameterized query support - fixes #793 #794
Changes from all commits
d565832
b82d8e2
a5c981c
e324436
9ba1241
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,21 @@ | |
import uuid | ||
from datetime import date | ||
from enum import Enum | ||
from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, Optional, Sequence, Set, Type, TypeVar, Union | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Any, | ||
Callable, | ||
Iterable, | ||
Iterator, | ||
List, | ||
Optional, | ||
Sequence, | ||
Set, | ||
Tuple, | ||
Type, | ||
TypeVar, | ||
Union, | ||
) | ||
|
||
from pypika.enums import Arithmetic, Boolean, Comparator, Dialects, Equality, JSONOperators, Matching, Order | ||
from pypika.utils import ( | ||
|
@@ -288,57 +302,111 @@ def get_sql(self, **kwargs: Any) -> str: | |
raise NotImplementedError() | ||
|
||
|
||
def idx_placeholder_gen(idx: int) -> str: | ||
return str(idx + 1) | ||
|
||
|
||
def named_placeholder_gen(idx: int) -> str: | ||
return f'param{idx + 1}' | ||
|
||
|
||
class Parameter(Term): | ||
is_aggregate = None | ||
|
||
def __init__(self, placeholder: Union[str, int]) -> None: | ||
super().__init__() | ||
self.placeholder = placeholder | ||
self._placeholder = placeholder | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be good to use a setter here. With that, implementing a check for the correct type can happen on initialization if put in the setter There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow how to do this pythonically? Could you share some pseudo code to show what you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mvanderlee I believe @wd60622 was referring to the following: @property
def placeholder(self):
return self._placeholder
@placeholder.setter
def placeholder(self, placeholder):
# can add checks here if needed
self._placeholder = placeholder There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, which adds no value and isn't pythonic. This isn't Java/C#. It would also break parameters. After a parameter has been created and used, any changes to the placeholder will break queries. Which is why I've made it private with a getter only. I haven't changed the type, so any type checks would have to be part of a separate PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would disagree with you on it not being Pythonic, as it’s a native language feature. Also, I was trying to be helpful, but given your receptiveness to discussion and tone, I won’t proceed. Good luck. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for my tone @danielenricocahall it'd been a rough week and I lashed out. As for pythonic. This video does a good job at explaining language feature vs pythonic. |
||
|
||
@property | ||
def placeholder(self): | ||
return self._placeholder | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return str(self.placeholder) | ||
|
||
def update_parameters(self, param_key: Any, param_value: Any, **kwargs): | ||
pass | ||
|
||
class QmarkParameter(Parameter): | ||
"""Question mark style, e.g. ...WHERE name=?""" | ||
def get_param_key(self, placeholder: Any, **kwargs): | ||
return placeholder | ||
|
||
def __init__(self) -> None: | ||
pass | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return "?" | ||
class ListParameter(Parameter): | ||
def __init__(self, placeholder: Union[str, int, Callable[[int], str]] = idx_placeholder_gen) -> None: | ||
super().__init__(placeholder=placeholder) | ||
self._parameters = list() | ||
|
||
@property | ||
def placeholder(self) -> str: | ||
if callable(self._placeholder): | ||
return self._placeholder(len(self._parameters)) | ||
|
||
class NumericParameter(Parameter): | ||
"""Numeric, positional style, e.g. ...WHERE name=:1""" | ||
return str(self._placeholder) | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return ":{placeholder}".format(placeholder=self.placeholder) | ||
def get_parameters(self, **kwargs): | ||
return self._parameters | ||
|
||
def update_parameters(self, value: Any, **kwargs): | ||
self._parameters.append(value) | ||
|
||
class NamedParameter(Parameter): | ||
"""Named style, e.g. ...WHERE name=:name""" | ||
|
||
class DictParameter(Parameter): | ||
def __init__(self, placeholder: Union[str, int, Callable[[int], str]] = named_placeholder_gen) -> None: | ||
super().__init__(placeholder=placeholder) | ||
self._parameters = dict() | ||
|
||
@property | ||
def placeholder(self) -> str: | ||
if callable(self._placeholder): | ||
return self._placeholder(len(self._parameters)) | ||
|
||
return str(self._placeholder) | ||
|
||
def get_parameters(self, **kwargs): | ||
return self._parameters | ||
|
||
def get_param_key(self, placeholder: Any, **kwargs): | ||
return placeholder[1:] | ||
|
||
def update_parameters(self, param_key: Any, value: Any, **kwargs): | ||
self._parameters[param_key] = value | ||
|
||
|
||
class QmarkParameter(ListParameter): | ||
def get_sql(self, **kwargs): | ||
return '?' | ||
|
||
|
||
class NumericParameter(ListParameter): | ||
"""Numeric, positional style, e.g. ...WHERE name=:1""" | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return ":{placeholder}".format(placeholder=self.placeholder) | ||
|
||
|
||
class FormatParameter(Parameter): | ||
class FormatParameter(ListParameter): | ||
"""ANSI C printf format codes, e.g. ...WHERE name=%s""" | ||
|
||
def __init__(self) -> None: | ||
pass | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return "%s" | ||
|
||
|
||
class PyformatParameter(Parameter): | ||
class NamedParameter(DictParameter): | ||
"""Named style, e.g. ...WHERE name=:name""" | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return ":{placeholder}".format(placeholder=self.placeholder) | ||
|
||
|
||
class PyformatParameter(DictParameter): | ||
"""Python extended format codes, e.g. ...WHERE name=%(name)s""" | ||
|
||
def get_sql(self, **kwargs: Any) -> str: | ||
return "%({placeholder})s".format(placeholder=self.placeholder) | ||
|
||
def get_param_key(self, placeholder: Any, **kwargs): | ||
return placeholder[2:-2] | ||
|
||
|
||
class Negative(Term): | ||
def __init__(self, term: Term) -> None: | ||
|
@@ -385,9 +453,44 @@ def get_formatted_value(cls, value: Any, **kwargs): | |
return "null" | ||
return str(value) | ||
|
||
def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", **kwargs: Any) -> str: | ||
sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs) | ||
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs) | ||
def _get_param_data(self, parameter: Parameter, **kwargs) -> Tuple[str, str]: | ||
param_sql = parameter.get_sql(**kwargs) | ||
param_key = parameter.get_param_key(placeholder=param_sql) | ||
|
||
return param_sql, param_key | ||
|
||
def get_sql( | ||
self, | ||
quote_char: Optional[str] = None, | ||
secondary_quote_char: str = "'", | ||
parameter: Parameter = None, | ||
**kwargs: Any, | ||
) -> str: | ||
if parameter is None: | ||
sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs) | ||
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs) | ||
|
||
# Don't stringify numbers when using a parameter | ||
if isinstance(self.value, (int, float)): | ||
value_sql = self.value | ||
else: | ||
value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) | ||
param_sql, param_key = self._get_param_data(parameter, **kwargs) | ||
parameter.update_parameters(param_key=param_key, value=value_sql, **kwargs) | ||
|
||
return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs) | ||
|
||
|
||
class ParameterValueWrapper(ValueWrapper): | ||
def __init__(self, parameter: Parameter, value: Any, alias: Optional[str] = None) -> None: | ||
super().__init__(value, alias) | ||
self._parameter = parameter | ||
|
||
def _get_param_data(self, parameter: Parameter, **kwargs) -> Tuple[str, str]: | ||
param_sql = self._parameter.get_sql(**kwargs) | ||
param_key = self._parameter.get_param_key(placeholder=param_sql) | ||
|
||
return param_sql, param_key | ||
|
||
|
||
class JSON(Term): | ||
|
@@ -551,6 +654,7 @@ def __init__( | |
if isinstance(table, str): | ||
# avoid circular import at load time | ||
from pypika.queries import Table | ||
|
||
table = Table(table) | ||
self.table = table | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type hint here doesn't include callable? Is that correct still?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, only ListParameter and DictParameter (and classes that inherit from them) support callable.
The base parameter does not.