The solution suggested by blhsing is great, I would only add some improvements:
- you might also want to ignore mulit-line comments between
/*
and */
- using
:(\w*)
instead of :(\w+)
in the pattern allows you to catch ill-formatted queries with isolated colons such as SELECT id =: id FROM table
(note the whitespace between :
id
).
- The three-fold nested function
to_positional
is quite complex and hard to understand. Maybe just provide a function that accepts a query in named parameter style and a dict of parameters and returns an equivalent query in ?-style along with a tuple of parameter values.
For instance, adjust the to_positional
function as follows:
import re
def to_positional(query:str, params:dict)->tuple[str, tuple]:
"""Parse (query, params)-pair in named parameter style into equivalent
(query, params) pair in qmark-style.
"""
pattern = re.compile(r"'(?:''|[^'])*'|--.*|/\*[\s\S]*?\*/|:(\w*)")
p = []
def replacer(match):
name = match[1]
if name is None:
return match[0]
elif len(name)==0:
raise SyntaxError("Cannot parse isolated ':'.")
p.append(params[name])
return '?'
query = pattern.sub(replacer, query)
return query, p
Then use it to parse any (query, param:dict) pair in named parameter style to (query, param:tuple) in ?-style like so:
query = """
SELECT
-- asdf = : asdf, -- Uncomment this line to provoke a SyntaxError
id = :id,
name = :name,
price = :price, -- :comment
description = ':description ''' || :description || ''''
/* WHERE
id=:id
AND description= :description
*/
;"""
params = {'id': 123, 'name': 'foo', 'price': 99.99, 'description': 'foo description'}
query, params = to_positional(query, params)
print(query, params)
returns
SELECT
-- asdf = : asdf, -- Uncomment this line to provoke a SyntaxError
id = ?,
name=?,
price=?, -- :comment
description=':description ''' || ? || ''''
/* WHERE
id=:id
AND description= :description
*/
;
[123, 'foo', 99.99, 'foo description']