I am trying to create a methodology for passing parameters automatically through something like locals()
, similarly to how f-strings work.
How it currently works
import pandas as pd def my_func(conn, string_id, date, integer_ids): sql = f""" select * from RISK a where STRING_ID = '{string_id}' and DATE = {date} and INTEGER_ID in ({','.join(map(str, integer_ids))})""" df = pd.read_sql(sql, conn) return df
However, this approach means I cannot copy-paste the SQL into SQL developer or similar, and run it from there. So I would like an approach that makes use of parameters instead.
There seems to be two problems with that
- Parameters must be literals, so its not possible to pass along lists
- I need to create a dictionary manually, and cannot simply pass something like
locals()
How I would like it to work would be something like the example below (which obviously doesn’t work)
import pandas as pd def my_func(conn, string_id, date, integer_ids): sql = """ select * from RISK where STRING_ID = :string_id and DATE = :date and INTEGER_ID in :integer_ids""" df = pd.read_sql(sql, conn, params=locals()) return df
EDIT: After testing a bit, maybe I could use a regex to find all instances of :param
and replace them with the parameter value, e.g.
import re pattern = '[nr].*:s*([^nr]*)' matches = re.findall(pattern,sql) for match in matches: sql = sql.replace(':'+match, eval(match))
It’s just not very pretty, and it introduces issues related to object types. E.g. the string_id should be encapsulated by '
, and the date needs to be converted to a string object as well
Final edit: Thanks to perl, I now have a working solution to my problem
def read_sql(sql, conn, params): # Finds all words following a ":" sign in the sql for p in re.findall(':(w+)', sql): if isinstance(params.get(p), (tuple, list)): ext_params = {f'{p}_{i:03d}': p_i for i, p_i in enumerate(params.get(p))} sql = sql.replace(f':{p}', f"(:{', :'.join(ext_params)})") params.update(ext_params) sql_text = sqlalchemy.text(sql) return pd.read_sql(sql_text, conn, params=params) def my_func(conn, string_id, date, integer_ids): sql = """ select * from RISK where STRING_ID = :string_id and DATE = :date and INTEGER_ID in :integer_ids""" df = read_sql(sql, conn, locals()) return df
Advertisement
Answer
You can use parametrized queries by wrapping the query in sqlalchemy.text
and converting lists to tuples. For example:
def my_func(conn, min_number, letters): # convert lists to tuples letters = tuple(letters) # wrap sql in sqlalchemy.text sql = sqlalchemy.text(""" SELECT * FROM letters WHERE number >= :min_number AND letter in :letters""") # read and return the resulting dataframe df = pd.read_sql(sql, conn, params=locals()) return df my_func(conn, 10, ['a', 'b', 'c', 'x', 'y', 'z'])
Output:
letter number 0 x 23 1 y 24 2 z 25
For completeness of the example, the following was used as a test table:
df = pd.DataFrame({ 'letter': list(string.ascii_lowercase), 'number': range(len(string.ascii_lowercase))}) df.to_sql('letters', conn, index=False)
Update: Here’s a possible workaround for Oracle to make it work with lists:
def get_query(sql, **kwargs): for k, v in kwargs.items(): vs = "','".join(v) sql = sql.replace(f':{k}', f"('{vs}')") return sql def my_func(conn, min_number, letters): sql_template = """ SELECT * FROM letters WHERE number >= :min_number AND letter in :letters """ # pass list variables to `get_query` function as named parameters # to get parameters replaced with ('value1', 'value2', ..., 'valueN') sql = sqlalchemy.text( get_query(sql_template, letters=letters)) df = pd.read_sql(sql, conn, params=locals()) return df my_func(conn, 10, ['a', 'b', 'c', 'x', 'y', 'z'])
Update 2: Here’s the get_query
function that works with both strings and numbers (enclosing in quotes strings, but not numbers):
def get_query(sql, **kwargs): # enclose in quotes strings, but not numbers def q(x): q = '' if isinstance(x, (int, float)) else "'" return f'{q}{x}{q}' # replace with values for k, v in kwargs.items(): sql = sql.replace(f':{k}', f"({','.join([q(x) for x in v])})") return sql
For example:
sql = """ SELECT * FROM letters WHERE number in :numbers AND letters in :letters """ get_query(sql, numbers=[1, 2, 3], letters=['A', 'B', 'C'])
Output:
SELECT * FROM letters WHERE number in (1,2,3) AND letters in ('A','B','C')