-
Notifications
You must be signed in to change notification settings - Fork 305
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
Basic parametrised query generation (WIP) #201
Conversation
Hi, thanks for the PR. This feature definitely a big ask from a lot of people, so appreciate you contributing it. So far the change looks good, let me know if you need anything in order to check off the remaining boxes. |
I did some more digging on the 110 tests that fail due to I started changing some tests, (I estimate ~500loc diff just to change each of these tests) and found that negation (from Please advise what you think is the best course of action re this? |
Other than API breakage, removing |
I think it would be really helpful if you would start with some basic tests covering the syntax you would like to use for parameters so it is clear how the pypika query should be built up and the how the SQL generated by it should look. Using How to use variables in SQL statement in Python? as a reference, it appears that different db vendors use different markers for parameters, so should aim for something that gives the user control over that. I'm not sure why you would need a second get_sql function to generate these queries. To support paramterized queries, you basically need a query element that serializes as the marker in the query like I think a syntax like the following would work real well and be easy to implement without needing to change so much of the internals:
would generate SQL like
Then the user can decide which marker to use for each variable, can freely reuse them, and use them directly with the python db API.
|
Yes, having a The question is, right now (whilst I was writing tests) it seems that evaluation happens in the order one does calls, but will this always be the case? |
So far I found: That is a lot of variance as to how the parameters get defined. Maybe your suggestion of just expecting a string value with the right value is the most sane one? |
I would hesitate to try and generate which parameter is which because you ultimately can never know how the user will want to use it. For example, what if they want to reference the same parameter in two places within the query. Giving them control over which reference is used for each parameter will 1) force them to use the right one for their database platform and 2) give them full control over how the parameters are used in the query. To answer your question, I had it in mind to refactor the way the SQL is generated at some point since that is by a long shot the most complex part of this library, but I don't think that would change the order of how constituents of the query are serialized. You can probably safely assume, for example, that if multiple WHERE-clauses are used, they will be serialized in the SQL in the exact same order. |
I was thinking the same. I had to do some research to get data, and two things make it a bad idea to generate inside PyPika:
So I think a very simple |
@twheys Please review. |
Looks great. Approved, merged, and published. Appreciate you added py37 as well. Thanks 👍 |
I see the point of having explicit q = Query.from_(table).select("*").where(table.id == "?")
sql = str(q).replace("'?'", "?") Maybe we add some kind of hook to the end of
This way people themselves could decide how to parse placeholders for their particular driver, with regex or something. What do you think? |
The current implementation does not work for us. We have a complex where clause builder that would need to be completely rewritten to support the current parameterization. We solved our issue by patching the ValueWrapper as follows: from typing import Any, List, Optional
from pypika.terms import ValueWrapper
from pypika.utils import format_alias_sql
class ParameterizedValueWrapper(ValueWrapper):
def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", param_wrapper: ParameterWrapper = None, **kwargs: Any) -> str:
if param_wrapper 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)
else:
value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
param_sql = param_wrapper.get_sql(**kwargs)
param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
class ParameterWrapper:
def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
pass
def get_sql(self, **kwargs):
pass
class QParameterWrapper(ParameterWrapper):
def __init__(self, parameters: List[Any]):
self.parameters = parameters
def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
self.parameters.append(param_value)
def get_sql(self, **kwargs):
return '?'
class NamedParameterWrapper(ParameterWrapper):
def __init__(self, parameters: Dict[str, Any]):
self.parameters = parameters
def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
self.parameters[param_key[1:]] = param_value
def get_sql(self, **kwargs):
return f':param{len(self.parameters) + 1}' Usage: pypika.terms.ValueWrapper = ParameterizedValueWrapper
q = pypika.Query.from_('test').select('*').where((pypika.Field('name') == 'foobar') & (pypika.Field('foo') != 'bar'))
parameters = []
sql = q.get_sql(param_wrapper=QParameterWrapper(parameters))
print(sql)
print(parameters)
parameters = {}
sql = q.get_sql(param_wrapper=NamedParameterWrapper(parameters))
print(sql)
print(parameters) Output:
|
This PR adds parametrized query building by adding a
Parameter(':1')
term that will be the parameter placeholder.Due to placeholders in SQL for parametrized queries being so varied (even between drivers or setup for the same dialect) that I decided to leave implementation as explicit as possible.
Whatever is indicated as the string is included verbatim into the SQL.
An example of this madness:
PostgreSQL →
$number
OR%s
+:name
(depending on driver)MySQL →
%s
SQLite →
?
Vertica →
:name
Oracle →
:number
+:name
MSSQL →
%(name)s
OR:name
+:number
(depending on driver)This PR is a work-in-progress:
Parameter()
term, and added to root__init__.py
for convenience__str__()
where the parent has the same variant implemented.Fixes #113, potential workaround for some of #3