0

Using the below code as an example, sometimes I want to be able to see what values are being entered into a select statement to verify the query is accurate. How do I do this so that what gets printed would look something like this: SQL with values: SELECT column FROM table WHERE id='1234' and name='bob'

sql = "SELECT column FROM table WHERE id=%s and name=%s"
args = [id, name]
query_string = ???
print('SQL with values: ' + query_string)
with connection.cursor() as cursor:
    cursor.execute(sql, args)
    result = cursor.fetchone()
Chris Utter
  • 143
  • 2
  • 2
  • 10
  • Which SQL package are you using? They typically expect a tuple of args, not an array. – jarmod Sep 21 '22 at 20:29
  • Related: https://stackoverflow.com/questions/5266430/how-to-see-the-real-sql-query-in-python-cursor-execute-using-pyodbc-and-ms-acces – jarmod Sep 21 '22 at 20:42
  • I'm using pymysql – Chris Utter Sep 21 '22 at 22:49
  • I was able to create a bespoke solution for PyMySQL below. It uses some implementation specifics that aren't in the documentation, so it isn't portable to other packages. – brunson Sep 22 '22 at 16:19

3 Answers3

3

You can use this

sql = "SELECT column FROM table WHERE id=%s and name=%s"
args = [id, name]
query_string = sql % (id, name)
print('SQL with values: ' + query_string)
with connection.cursor() as cursor:
    cursor.execute(sql, args)
    result = cursor.fetchone()
jarmod
  • 71,565
  • 16
  • 115
  • 122
tonno7103
  • 57
  • 4
  • 1
    That's missing the point. – Chris Utter Sep 21 '22 at 20:29
  • 1
    @ChrisUtter you should explain why you think it's missing the point. It's not, as far as I can see, but perhaps you're thinking of something else. – jarmod Sep 21 '22 at 20:32
  • Do you want to do the reverse thing? or..? – tonno7103 Sep 21 '22 at 20:32
  • Using percent string formatting will not show the query quoted properly. – brunson Sep 21 '22 at 20:41
  • @jarmod you edited your initial response. What you have now is closer, but I would like the quotations inserted in the query so if so desired I can copy and paste the query and run it without needing to edit it first. – Chris Utter Sep 21 '22 at 21:04
  • @brunson the whole point of using SQL parameters is that it's impossible to quote a query properly. – Mark Ransom Sep 21 '22 at 21:05
  • It isn't impossible, it's just easy to mess up and allow injection attacks, which is why it's advisable to rely on tested code from the modules rather than write your own. – brunson Sep 21 '22 at 21:11
  • In the connector from mysql it seems to happen around here for the `cursor.execute()` method, https://github.com/mysql/mysql-connector-python/blob/master/lib/mysql/connector/cursor.py#L539. But I can't seem to find a way that they've exposed the functionality to be easy to call outside of the execute. – brunson Sep 21 '22 at 21:20
  • Here: https://github.com/mysql/mysql-connector-python/blob/master/lib/mysql/connector/cursor.py#L555 they assign the processed sql statement to `self._executed` which could be printed, but that's only after the execution. If you only want to know what WAS executed, that's what you'd like to print. If you want to display what will or would be executed, you'd need to write some code for it. – brunson Sep 21 '22 at 21:28
  • If you know which args are strings: `query_string = sql % (id, f"\'{name}\'")`. But definitely do not execute this as a query. Only use it for debugging. – jarmod Sep 21 '22 at 21:41
0
# ...
query_string = sql % (args[0], "\"" + args[1] + "\"")
# ...
erjemin
  • 69
  • 7
0

If you're using the official mysql-connector-python package from MySQL and you're satisfied with printing the exact SQL after it was executed, then print(cursor._executed).

They don't seem to expose a simple processing function to display what would be executed, you'd have to lift some code from around here: https://github.com/mysql/mysql-connector-python/blob/master/lib/mysql/connector/cursor.py#L539 and make your own function if you need that.

You could approximate it somewhat with code like this:

def render_sql(sql, args):
    quoted = list()
    for arg in args:
        if type(arg) in (int, float):
            quoted.append(str(arg))
        elif type(arg) is str:
            quoted.append(f"'{arg}'")
        else:
            raise Exception(f"got an unknown type: {type(arg} for SQL param: {arg}")

    return sql % tuple(quoted)

sql = "SELECT column FROM table WHERE id=%s and name=%s"
print(render_sql(sql, (1, 1)))
# SELECT column FROM table WHERE id=1 and name=1
print(render_sql(sql, (1, 1.5)))
# SELECT column FROM table WHERE id=1 and name=1.5
print(render_sql(sql, (1, "test")))
# SELECT column FROM table WHERE id=1 and name='test'
print(render_sql(sql, (1, "text with spaces")))
# SELECT column FROM table WHERE id=1 and name='text with spaces'

You can extend that with additional types. But, as jarmod said, use this only for debugging, do not use it to execute on the database without reviewing the rendered SQL first.

Update:

I came up with this, but it's specific to the PyMySQL implementation and not general:

from pymysql.connections import Connection

def render_sql(operation, params):
    connection = Connection(defer_connect=True)
    connection.server_status = 0
    return connection.cursor().mogrify(operation, params)

So this:

render_sql(
    "SELECT * FROM Users WHERE id = %s and name = %s and password = %s",
    (1, "user", "I'm past")
)

yields:

SELECT * FROM Users WHERE id = 1 and name = 'user' and password = 'I\'m past'

Security Demo:

For those that aren't familiar with SQL Injection attacks.

I used the above function and a naive implementation where the %s placeholders are simply replaced with the values passed in.

def naive_sql(operation, params):
    return operation % params

Two "parameterized" queries:

good = "SELECT * FROM Users WHERE name = %s AND password = %s"
bad =  "SELECT * FROM Users WHERE name = '%s' AND password = '%s'"

Example 1:

params = ("bobby", "PaSsWoRd")
print("Good:", render_sql(good, params))
print(" Bad:", naive_sql(bad, params))
print("so far so good")

Output:

Good: SELECT * FROM Users WHERE name = 'bobby' AND password = 'PaSsWoRd'
 Bad: SELECT * FROM Users WHERE name = 'bobby' AND password = 'PaSsWoRd'
so far so good

Example 2:

params = ("bobby", "' OR 'a' = 'a")
print("Good:", render_sql(good, params))
print(" Bad:", naive_sql(bad, params))
print("just granted access without a password")

Output:

Good: SELECT * FROM Users WHERE name = 'bobby' AND password = '\' OR \'a\' = \'a'
 Bad: SELECT * FROM Users WHERE name = 'bobby' AND password = '' OR 'a' = 'a'
just granted access without a password

Example 3:

params = ("bobby", "' ; UPDATE USERS SET password = 'letmein' WHERE name = 'bobby' ; -- ")
print("Good:", render_sql(good, params))
print(" Bad:", naive_sql(bad, params))
print("updated bobby's password")

Output:

Good: SELECT * FROM Users WHERE name = 'bobby' AND password = '\' ; UPDATE USERS SET password = \'letmein\' WHERE name = \'bobby\' ; -- '
 Bad: SELECT * FROM Users WHERE name = 'bobby' AND password = '' ; UPDATE USERS SET password = 'letmein' WHERE name = 'bobby' ; -- '
updated bobby's password

Example 4:

params = ("bobby", "' ; DROP TABLE Users ; -- ")
print("Good:", render_sql(good, params))
print(" Bad:", naive_sql(bad, params))
print("your site is now down")

Output:

Good: SELECT * FROM Users WHERE name = 'bobby' AND password = '\' ; DROP TABLE Users ; -- '
 Bad: SELECT * FROM Users WHERE name = 'bobby' AND password = '' ; DROP TABLE Users ; -- '
your site is now down

Obligatory XKCD: https://xkcd.com/327/

brunson
  • 749
  • 4
  • 13
  • I tried doing a method like this but for some reason couldn't get it to work. Your version works perfect and is exactly what I needed. Thanks for the help. – Chris Utter Sep 22 '22 at 14:23
  • I added a solution for PyMySQL and a bunch of security related stuff. – brunson Sep 22 '22 at 16:21