Escape SQL "LIKE" value for Postgres with psycopg2
Asked Answered
N

13

56

Does psycopg2 have a function for escaping the value of a LIKE operand for Postgres?

For example I may want to match strings that start with the string "20% of all", so I want to write something like this:

sql = '... WHERE ... LIKE %(myvalue)s'
cursor.fetchall(sql, { 'myvalue': escape_sql_like('20% of all') + '%' }

Is there an existing escape_sql_like function that I could plug in here?

(Similar question to How to quote a string value explicitly (Python DB API/Psycopg2), but I couldn't find an answer there.)

Nonsuch answered 21/1, 2010 at 0:58 Comment(1)
What if you want to query ... WHERE ... LIKE %(myvalue)%, where myvalue is string?Unwary
A
39

Yeah, this is a real mess. Both MySQL and PostgreSQL use backslash-escapes for this by default. This is a terrible pain if you're also escaping the string again with backslashes instead of using parameterisation, and it's also incorrect according to ANSI SQL:1992, which says there are by default no extra escape characters on top of normal string escaping, and hence no way to include a literal % or _.

I would presume the simple backslash-replace method also goes wrong if you turn off the backslash-escapes (which are themselves non-compliant with ANSI SQL), using NO_BACKSLASH_ESCAPE sql_mode in MySQL or standard_conforming_strings conf in PostgreSQL (which the PostgreSQL devs have been threatening to do for a couple of versions now).

The only real solution is to use the little-known LIKE...ESCAPE syntax to specify an explicit escape character for the LIKE-pattern. This gets used instead of the backslash-escape in MySQL and PostgreSQL, making them conform to what everyone else does and giving a guaranteed way to include the out-of-band characters. For example with the = sign as an escape:

# look for term anywhere within title
term= term.replace('=', '==').replace('%', '=%').replace('_', '=_')
sql= "SELECT * FROM things WHERE description LIKE %(like)s ESCAPE '='"
cursor.execute(sql, dict(like= '%'+term+'%'))

This works on PostgreSQL, MySQL, and ANSI SQL-compliant databases (modulo the paramstyle of course which changes on different db modules).

There may still be a problem with MS SQL Server/Sybase, which apparently also allows [a-z]-style character groups in LIKE expressions. In this case you would want to also escape the literal [ character with .replace('[', '=['). However according to ANSI SQL escaping a character that doesn't need escaping is invalid! (Argh!) So though it will probably still work across real DBMSs, you'd still not be ANSI-compliant. sigh...

Ahmed answered 21/1, 2010 at 2:4 Comment(1)
standard_conforming_strings in postgres does not break backslash escapes in like queries. At least not in 12.7.Zip
A
37

I was able to escape % by using %% in the LIKE operand.

sql_query = "select * from mytable where website like '%%.com'"
cursor.fetchall(sql_query)
Adulteration answered 27/8, 2019 at 22:48 Comment(3)
This should imho be the solution. Is there anything wrong with it?Tollmann
Maybe this is PostgreSQL version-dependent? This answer answer came several years after the question and the current top answer was posted.Paphian
I moved heaven and Earths to solve the issue but only this method worked. Much thanks!Shatter
F
8

If you're using a prepared statement, then the input will be wrapped in '' to prevent sql injection. This is great, but also prevents input + sql concatenation.

The best and safest way around this would be to pass in the %(s) as part of the input.

cursor.execute('SELECT * FROM goats WHERE name LIKE %(name)s', { 'name': '%{}%'.format(name)})
Flattie answered 20/11, 2019 at 8:55 Comment(1)
NOT what I was expecting to work, but all the other ways failed for me when I have other %s in the same query AND string substitution using .replace(). Thanks. Helped me a ton!Lytic
C
5

You can also look at this problem from a different angle. What do you want? You want a query that for any string argument executes a LIKE by appending a '%' to the argument. A nice way to express that, without resorting to functions and psycopg2 extensions could be:

sql = "... WHERE ... LIKE %(myvalue)s||'%'"
cursor.execute(sql, { 'myvalue': '20% of all'})
Culinarian answered 13/2, 2013 at 10:1 Comment(5)
that would match strings like 2001 had worst of all terrorismDuodenary
No, it would not because the % in the argument would be quoted.Culinarian
there's nothing telling pscopg2 that % needs special treatment.Duodenary
It is a bound variable: it is quoted by default.Culinarian
yeah, which is why it won't work. you should test it.Duodenary
L
5

I found a better hack. Just append '%' to your search query_text.

con, queryset_list = psycopg2.connect(**self.config), None
cur = con.cursor(cursor_factory=RealDictCursor)
query = "SELECT * "
query += " FROM questions WHERE  body LIKE %s OR title LIKE %s  "
query += " ORDER BY questions.created_at"
cur.execute(query, ('%'+self.q+'%', '%'+self.q+'%'))
Lyrist answered 27/8, 2018 at 18:14 Comment(0)
A
3

I wonder if all of the above is really needed. I am using psycopg2 and was simply able to use:

data_dict['like'] = psycopg2.Binary('%'+ match_string +'%')
cursor.execute("SELECT * FROM some_table WHERE description ILIKE %(like)s;", data_dict)
Adamski answered 2/6, 2010 at 6:3 Comment(1)
It can ve even easier: `cursor.execute("SELECT * FROM some_table WHERE description LIKE %s;", ['foobar%']);Adrien
E
2

Instead of escaping the percent character, you could instead make use of PostgreSQL's regex implementation.

For example, the following query against the system catalogs will provide a list of active queries which are not from the autovacuuming sub-system:

SELECT procpid, current_query FROM pg_stat_activity
WHERE (CURRENT_TIMESTAMP - query_start) >= '%s minute'::interval
AND current_query !~ '^autovacuum' ORDER BY (CURRENT_TIMESTAMP - query_start) DESC;

Since this query syntax doesn't utilize the 'LIKE' keyword, you're able to do what you want... and not muddy the waters with respect to python and psycopg2.

Elamitic answered 6/2, 2014 at 18:2 Comment(0)
N
1

Having failed to find a built-in function so far, the one I wrote is pretty simple:

def escape_sql_like(s):
    return s.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
Nonsuch answered 21/1, 2010 at 1:22 Comment(2)
@JensTimmerman this function only escape the like tokens, to use the normal string escaping on the result before using it in a query. correct string escaping depends on the sessing standard_conforming_stings and so is best done using the library code.Duodenary
More concisely, re.sub(r'([%\\"\'_])', r'\\\1', s)Adkins
A
1

From 2023, Here is how I do it with psycopg3

query = f'''SELECT * FROM table where column like %s;'''
cursor.execute(query, f'%{my_value}%')
Adamson answered 12/7, 2023 at 3:12 Comment(0)
K
0

You can create a Like class subclassing str and register an adapter for it to have it converted in the right like syntax (e.g. using the escape_sql_like() you wrote).

Karlakarlan answered 16/2, 2010 at 10:45 Comment(1)
An interesting idea that I hadn't thought of, but you would invariably need to combine the escaped string with real LIKE operators (% or _), otherwise you might as well have used = instead of LIKE. If you do that then I'm not sure what the benefit of this approach is over the simpler approach of just calling the escape function.Nonsuch
A
0

I made some modifications to the code above to do the following:

def escape_sql_like(SQL):
    return SQL.replace("'%", 'PERCENTLEFT').replace("%'", 'PERCENTRIGHT')

def reescape_sql_like(SQL):
    return SQL.replace('PERCENTLEFT', "'%").replace('PERCENTRIGHT', "%'")

SQL = "SELECT blah LIKE '%OUCH%' FROM blah_tbl ... "
SQL = escape_sql_like(SQL)
tmpData = (LastDate,)
SQL = cur.mogrify(SQL, tmpData)
SQL = reescape_sql_like(SQL)
cur.execute(SQL)
Aculeus answered 4/8, 2011 at 4:48 Comment(0)
F
0

It just requires to concatenate double % before and after it. Using "ilike" instead of "like" makes it case insensitive.

query = """
    select 
        * 
    from 
        table 
    where 
        text_field ilike '%%' || %(search_text)s || '%%'
"""
Fagaceous answered 19/11, 2021 at 21:51 Comment(2)
I'm not seeing this work: SELECT 1 WHERE '20th of May' LIKE '%%20% of%%' still returns 1 despite the fact that the string doesn't contain a percent sign.Allanallana
or is it a feature specific to that library?Allanallana
B
-3

I think it would be simpler and more readable to use f-strings.

query = f'''SELECT * FROM table where column like '%%{my_value}%%' '''
cursor.execute(query)
Bullpen answered 7/8, 2020 at 21:8 Comment(2)
Never do this, this is insecure. Introduces a risk of an SQL Injection.Absenteeism
Well, it's just a example like anothers from this question who use the same approach. It's not abou security but how to escape % in query. And this is only a security risk if you use the raw input from the user or something like this.Bullpen

© 2022 - 2024 — McMap. All rights reserved.