From f0a633c428b88ddefa335a093682e64fe2f52f3c Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sun, 3 Jul 2022 03:49:15 +0100 Subject: [PATCH] docs: add literal strings examples Move the PEP 675 section to the typing page. --- docs/advanced/typing.rst | 52 ++++++++++++++++++++++++++++++++++++++++ docs/basic/params.rst | 22 +++++------------ 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/docs/advanced/typing.rst b/docs/advanced/typing.rst index 9b58372a8..df6483b71 100644 --- a/docs/advanced/typing.rst +++ b/docs/advanced/typing.rst @@ -126,3 +126,55 @@ any issue. Pydantic will also raise a runtime error in case the print(f"{p.first_name} was born in {p.dob.year}") else: print(f"Who knows when {p.first_name} was born") + + +.. _literal-string: + +Checking literal strings in queries +----------------------------------- + +The `~Cursor.execute()` method and similar should only receive a literal +string as input, according to :pep:`675`. This mean that the query should +come from a literal string in your code, not from an arbitrary string +expression. + +For instance, passing an argument to the query should be done via the second +argument to `!execute()`, not by string composition: + +.. code:: python + + def get_record(conn: psycopg.Connection[Any], id: int) -> Any: + cur = conn.execute("SELECT * FROM my_table WHERE id = %s" % id) # BAD! + return cur.fetchone() + + # the function should be implemented as: + + def get_record(conn: psycopg.Connection[Any], id: int) -> Any: + cur = conn.execute("select * FROM my_table WHERE id = %s", (id,)) + return cur.fetchone() + +If you are composing a query dynamically you should use the `sql.SQL` object +and similar to escape safely table and field names. The parameter of the +`!SQL()` object should be a literal string: + +.. code:: python + + def count_records(conn: psycopg.Connection[Any], table: str) -> int: + query = "SELECT count(*) FROM %s" % table # BAD! + return conn.execute(query).fetchone()[0] + + # the function should be implemented as: + + def count_records(conn: psycopg.Connection[Any], table: str) -> int: + query = sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(table)) + return conn.execute(query).fetchone()[0] + +At the time of writing, no Python static analyzer implements this check (`mypy +doesn't implement it`__, Pyre_ does, but `doesn't work with psycopg yet`__). +Once the type checkers support will be complete, the above bad statements +should be reported as errors. + +.. __: https://github.com/python/mypy/issues/12554 +.. __: https://github.com/facebook/pyre-check/issues/636 + +.. _Pyre: https://pyre-check.org/ diff --git a/docs/basic/params.rst b/docs/basic/params.rst index 27d804310..15b876543 100644 --- a/docs/basic/params.rst +++ b/docs/basic/params.rst @@ -179,6 +179,11 @@ argument of the `Cursor.execute()` method:: data = ("O'Reilly", ) cur.execute(SQL, data) # Note: no % operator +.. note:: + + Python static code checkers are not quite there yet, but, in the future, + it will be possible to check your code for improper use of string + expressions in queries. See :ref:`literal-string` for details. .. seealso:: @@ -186,21 +191,6 @@ argument of the `Cursor.execute()` method:: at :ref:`how Psycopg converts data types `. -Using `LiteralString` in queries --------------------------------- - -The `~Cursor.execute()` method and similar should only receive a literal -string as input, according to :pep:`675`. - -If you need to compose a query dynamically you should use `sql.SQL` and -similar methods. - -At the time of writing the feature is experimental and only checked by the -Pyre_ checker. - -.. _Pyre: https://pyre-check.org/ - - .. index:: pair: Binary; Parameters @@ -248,5 +238,5 @@ requesting binary results is a clear winner is when you have large binary data in the database, such as images:: cur.execute( - "select image_data from images where id = %s", [image_id], binary=True) + "SELECT image_data FROM images WHERE id = %s", [image_id], binary=True) data = cur.fetchone()[0] -- 2.47.2