10

With Python's DB API spec you can pass an argument of parameters to the execute() method. Part of my statement is a WHERE IN clause and I've been using a tuple to populate the IN. For example:

params = ((3, 2, 1), )
stmt = "SELECT * FROM table WHERE id IN %s"
db.execute(stmt, params)

But when I run into a situation where the parameter tuple is only a tuple of 1 item, the execute fails.

ProgrammingError: ERROR: syntax error at or near ")"
LINE 13: WHERE id IN (3,)

How can I get the tuple to work with clause properly?

Tony Locke
  • 454
  • 3
  • 9
John Giotta
  • 16,432
  • 7
  • 52
  • 82

6 Answers6

13

Edit: If you think this answer circumvents the built-in protections against SQL-injection attack you're mistaken; look more closely.

Testing with pg8000 (a DB-API 2.0 compatible Pure-Python interface to the PostgreSQL database engine):

This is the recommended way to pass multiple parameters to an "IN" clause.

params = [3,2,1]
stmt = 'SELECT * FROM table WHERE id IN (%s)' % ','.join('%s' for i in params)
cursor.execute(stmt, params)

Full example:

>>> from pg8000 import DBAPI
>>> conn = DBAPI.connect(user="a", database="d", host="localhost", password="p")
>>> c = conn.cursor()
>>> prms = [1,2,3]
>>> stmt = 'SELECT * FROM table WHERE id IN (%s)' % ','.join('%s' for i in prms)
>>> c.execute(stmt,prms)
>>> c.fetchall()
((1, u'myitem1'), (2, u'myitem2'), (3, u'myitem3'))
mechanical_meat
  • 163,903
  • 24
  • 228
  • 223
  • 1
    Correct me if I'm wrong, but doesn't your example only pass the first item only to the IN sub-clause? > SELECT * FROM table WHERE id IN (3) – John Giotta Feb 12 '10 at 18:06
  • 3
    This answer is dangerously wrong! Substituting parameters yourself in Python code, instead of letting the database driver do it, is a security hole called "SQL injection". Imagine if one of the items in the list were the string `"); DROP TABLE table; --"`. – rspeer Aug 16 '16 at 21:50
  • 1
    Protection from SQL injections isn't something you should add on to code that allows SQL injections. You will rarely get it right. Instead, you should not write the code that allows SQL injections. – rspeer Aug 16 '16 at 22:11
  • 4
    @rspeer: This isn't substituting parameters in Python code, it's generating placeholders in Python code and then letting the DB-API do the substitution into those placeholders. – BrenBarn May 09 '17 at 18:58
  • It might be a little less easy to misunderstand without the comprehension: `['%s'] * len(params)`. – Oliver Bock Sep 29 '18 at 10:01
  • I think it's better to use '?' instead of '%s', so: `select * from table where id in (%s)' % ','.join(['?'] * len(params))` – Michael Jan 30 '19 at 14:52
1

The error is coming from the comma after the 3. Just leave it off for the single values and you're set.

params = ((3), ... )
stmt = "SELECT * FROM table WHERE id IN %s"
db.execute(stmt, params)
Daniel
  • 617
  • 1
  • 7
  • 10
  • Yeah I know why the error happened, but I'm not building the tuple. The tuple is populated by another SQL result. So in passing, the single item tuple retains a hanging comma. – John Giotta Feb 12 '10 at 17:03
  • I also wanted to point out that a single item tuple must have a trailing comma. – John Giotta Feb 12 '10 at 17:18
  • Ah, I misunderstood. Well in that case you could use len() to get the length of the tuple and if its one the use tuple[0] to extract the value without the comma. – Daniel Feb 12 '10 at 17:31
1

This may not be an answer to exactly the question you asked, but I think it may solve the problem you have.

Python's DB-API doesn't seem to give you a way to pass tuples as safely substituted parameters. The accepted answer from bernie is using the Python % operator for substitution, which is unsafe.

However, you may not have to pass tuples as parameters, particularly when the tuple you want is the result of another SQL query (as you indicated to Daniel). Instead, you can use SQL subqueries.

If the set of IDs you want in your IN clause is the result of SELECT id FROM other_table WHERE use=true, for example:

stmt = "SELECT * FROM table WHERE id IN (SELECT id FROM other_table WHERE use=true)"
db.execute(stmt)

And this can be parameterized (the safe way), too. If the IDs you want to select are the ones with a given parent_id:

stmt = "SELECT * FROM table WHERE id IN (SELECT id FROM other_table WHERE parent_id=%s)"
params = (parent_id,)
db.execute(stmt, params)
rspeer
  • 3,539
  • 2
  • 25
  • 25
1

As the question said, the following will fail:

params = ((3, 2, 1), )
stmt = "SELECT * FROM table WHERE id IN %s"
db.execute(stmt, params)

Following the pg8000 docs the IN can be replaced with an ANY() to give the same result:

params = ((3, 2, 1), )
stmt = "SELECT * FROM table WHERE id = ANY(%s)"
db.execute(stmt, params)

This sends the query and parameters separately to the server, avoiding SQL injection attacks.

Tony Locke
  • 454
  • 3
  • 9
0

A solution with f-string.

params = [...]
stmt = f"SELECT * FROM table WHERE id IN ({','.join(['%s']*len(params ),)})"
db.execute(stmt, params)

If there is another param placeholder it will be like this

age = 18
params = [...]
stmt = f"SELECT * FROM table WHERE age>%s AND id IN ({','.join(['%s']*len(params ),)})"
db.execute(stmt, tuple([age] + params))
-1

The accepted answer risks SQL injection; you should never ever pass user input directly to the database. Instead, generate a query with the correct number of placeholders, then let pg8000 do the escaping:

params = [3,2,1]
# SELECT * from table where id in (%s,%s,%s)
stmt = 'SELECT * FROM table WHERE id IN ({})'.format(','.join(['%s']*len(params)))
cursor.execute(stmt, tuple(params))
kielni
  • 4,779
  • 24
  • 21
  • 2
    Isn't this the same as what the accepted answer does? It is also generating a query with the right number of placeholders, not substituting the params directly into the query string. – BrenBarn May 09 '17 at 18:55
  • For params = [3] you will get the SQL syntax error "IN(3,)" – hipertracker Nov 16 '22 at 00:18