From: Daniele Varrazzo Date: Sun, 25 May 2025 22:22:13 +0000 (+0100) Subject: docs: add preview of template strings query documentation X-Git-Tag: 3.3.0.dev1~40 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7982048af59da6e17598f9cd868b2c73272813d6;p=thirdparty%2Fpsycopg.git docs: add preview of template strings query documentation --- diff --git a/docs/basic/index.rst b/docs/basic/index.rst index bf9e27da4..df1d8e0db 100644 --- a/docs/basic/index.rst +++ b/docs/basic/index.rst @@ -19,6 +19,7 @@ the database ` or :ref:`loading data using COPY `. install usage params + tstrings adapt pgtypes transactions diff --git a/docs/basic/tstrings.rst b/docs/basic/tstrings.rst new file mode 100644 index 000000000..9adabd2eb --- /dev/null +++ b/docs/basic/tstrings.rst @@ -0,0 +1,169 @@ +.. currentmodule:: psycopg + +.. index:: + pair: Query; Template strings + +.. _template-strings: + +Template string queries +======================= + +.. versionadded:: 3.3 + +.. warning:: + + This is an experimental feature, still `under active development`__ and + only documented here for preview. Details may change before Psycopg 3.3 + release. + + .. __: https://github.com/psycopg/psycopg/pull/1054 + + Template strings are a Python language feature under active development + too, planned for release in Python 3.14. Template string queries are + currently tested in Python 3.14 beta 1. + +Psycopg can process queries expressed as `template strings`__ defined in +:pep:`750` and implemented for the first time in Python 3.14. + +.. __: https://docs.python.org/3.14/whatsnew/3.14.html#pep-750-template-strings + +Template strings are similar to f-strings__: they are string literals +interspersed with variables or expressions marked by ``{}``. They use a ``t`` +prefix instead of ``f``, and can be used to express queries: + +.. __: https://docs.python.org/3/tutorial/inputoutput.html#tut-f-strings + +.. code:: python + + cursor.execute(t"SELECT * FROM mytable WHERE id = {id}") + +The difference between the two is that f-strings are immediately evaluated by +Python and passed to the rest of the program as an already formatted regular +string; t-strings, instead, are evaluated by Psycopg, which has a chance to +process query parameters in a safe way. + +For example you can pass to a query some strings parameters, which may contain +unsafe characters such as ``'``, or come from untrusted sources such as web +form, and leave Psycopg to perform the right processing: + +.. code:: python + + cursor.execute( + t"INSERT INTO mytable (first_name, last_name) VALUES ({first_name}, {last_name})" + ) + +This statement has the same effect as a classic: + +.. code:: python + + cursor.execute( + "INSERT INTO mytable (first_name, last_name) VALUES (%s, %s)", + (first_name, last_name), + ) + +as Psycopg will be able to send parameters separately from the query, or to +compose the query on the client side using safe escaping rules, according to +the :ref:`type of cursor ` used, but it is has the same +readability of f-strings. + +Format specifiers +----------------- + +Format specifiers can be associated to template strings interpolation using a +``:`` in the placeholder, for example in ``{id:b}``. Psycopg supports a few +format specifiers specifying `how to pass a parameter to the server`__ and a few +format specifiers specifying how to compose query parts on the client, in a +way similar to what can be obtained using the `psycopg.sql` objects. + +.. __: http://localhost:8000/basic/params.html#binary-parameters-and-results + +The supported specifiers for parameter formats are: + +- ``s``: automatic parameter format, similar to using ``%s`` in a classic + query. This is the same effect of using no format specifier. +- ``b``: use the binary format to pass the parameter, similar to using ``%b`` + in a classic query. +- ``t``: use the text format to pass the parameter, similar to using ``%t`` + in a classic query. + +The supported specifiers for query composition are: + +- ``i``: the parameter is an identifier_, for example a table or column name. + The parameter must be a string or a `sql.Identifier` instance. +- ``l``: the parameter is a literal value, which will be composed to the + query on the client. This allows to parametrize statements that :ref:`don't + support parametrization in PostgreSQL `. +- ``q``: the parameter is a snippet of statement to be included verbatim in + the query. The parameter must be another template string or a `sql.SQL` + instance. + +.. _identifier: https://www.postgresql.org/docs/current/sql-syntax-lexical.html + #SQL-SYNTAX-IDENTIFIERS + + +.. _tstring-template-notify: + +Example: NOTIFY +^^^^^^^^^^^^^^^ + +The NOTIFY_ command takes a *channel* parameter (an identifier, so it must be +quoted with double quotes if it contains any non-alphanumeric character), and +a *payload* parameter as a string (which must be escaped with string syntax, +hence with single quotes). + +.. _NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html + +The :sql:`NOTIFY` command cannot be parametrized by PostgreSQL, so it must be +composed entirely on the server side. Using template strings this could be as +simple as: + +.. code:: python + + def send_notify(conn: Connection, channel: str, payload: str) -> None: + conn.execute(t"NOTIFY {channel:i}, {payload:l}") + +Calling the function with channel ``foo.bar`` and payload ``O'Reilly`` will +result in executing the statement ``NOTIFY "foo.bar", 'O''Reilly'``. + + +.. _tstring-template-nested: + +Example: nested templates +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A string template merges literal parts of the query with parameter. It is also +possible to pass templates to templates in order to compose more and more +complex and dynamic SQL statements. + +For example let's say we have an `!User` Python object mapping to an ``users`` +database table. We might want to implement a `!search()` function to return +users by a list of ids, by names pattern, by group. This function might +be written as: + +.. code:: python + + def search_users( + conn: Connection, + ids: Sequence[int] | None = None, + name_pattern: str | None = None, + group_id: int | None = None, + ) -> list[User]: + filters = [] + if ids is not None: + filters.append(t"u.id = any({list(ids)})") + if name_pattern is not None: + filters.append(t"u.name ~* {name_pattern}") + if group_id is not None: + filters.append(t"u.group_id = {group_id}") + + if not filters: + raise TypeError("please specify at least one search parameter") + joined = sql.SQL(" AND ").join(filters) + + cur = conn.cursor(row_factory=class_row(User)) + cur.execute(t"SELECT * FROM users AS u WHERE {joined:q}") + return cur.fetchall() + +In this example we have used the `sql.SQL.join()` method overload that takes a +list of templates and returns a joined template in order to create an AND-ed +sequence of conditions.