63

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.)

1
  • What if you want to query ... WHERE ... LIKE %(myvalue)%, where myvalue is string? Commented Mar 5, 2022 at 12:02

13 Answers 13

43

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

sql_query = "select * from mytable where website like '%%.com'"
cursor.fetchall(sql_query)
Sign up to request clarification or add additional context in comments.

3 Comments

This should imho be the solution. Is there anything wrong with it?
Maybe this is PostgreSQL version-dependent? This answer answer came several years after the question and the current top answer was posted.
I moved heaven and Earths to solve the issue but only this method worked. Much thanks!
40

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...

1 Comment

standard_conforming_strings in postgres does not break backslash escapes in like queries. At least not in 12.7.
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)})

2 Comments

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!
This answer doesn't work for eactly the reason OP stated. Consider the case where name = "5% body fat". This will match goats with name 'Billy Goat - 5 years old and too much body fat' because the % in name has not been escaped so still acts like a wildcard
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'})

5 Comments

that would match strings like 2001 had worst of all terrorism
No, it would not because the % in the argument would be quoted.
there's nothing telling pscopg2 that % needs special treatment.
It is a bound variable: it is quoted by default.
yeah, which is why it won't work. you should test it.
4

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+'%'))

1 Comment

This answer doesn't work for eactly the reason OP stated. Consider the case where self.q = "5% sugar". This will match questions containing 'Chocolate: 50kg sugar enough??' because the % in self.q has not been escaped so still acts like a wildcard
2

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)

1 Comment

It can ve even easier: `cursor.execute("SELECT * FROM some_table WHERE description LIKE %s;", ['foobar%']);
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.

Comments

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('_', '\\_')

2 Comments

@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.
More concisely, re.sub(r'([%\\"\'_])', r'\\\1', s)
1

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).

1 Comment

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.
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}%')

Comments

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)

Comments

-1

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 || '%%'
"""

2 Comments

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.
or is it a feature specific to that library?
-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)

2 Comments

Never do this, this is insecure. Introduces a risk of an SQL Injection.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.