]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Added documentation for the `sql` module
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 22 Nov 2020 02:33:36 +0000 (02:33 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 22 Nov 2020 04:31:10 +0000 (04:31 +0000)
docs/index.rst
docs/sql.rst [new file with mode: 0644]
psycopg3/psycopg3/sql.py

index 0e4890b10ab3dc9fbe6d86f8ad0e9befa21eba8d..b2a7245b15158e8007e36e79aabdc8c78c51a04b 100644 (file)
@@ -24,6 +24,7 @@ the COPY support.
     adaptation
     connection
     cursor
+    sql
     from_pg2
 
 
diff --git a/docs/sql.rst b/docs/sql.rst
new file mode 100644 (file)
index 0000000..9748a14
--- /dev/null
@@ -0,0 +1,141 @@
+`psycopg3.sql` -- SQL string composition
+========================================
+
+.. index::
+    double: Binding; Client-Side
+
+.. module:: psycopg3.sql
+
+The module contains objects and functions useful to generate SQL dynamically,
+in a convenient and safe way. SQL identifiers (e.g. names of tables and
+fields) cannot be passed to the `~psycopg3.Cursor.execute()` method like query
+arguments::
+
+    # This will not work
+    table_name = 'my_table'
+    cur.execute("insert into %s values (%s, %s)", [table_name, 10, 20])
+
+The SQL query should be composed before the arguments are merged, for
+instance::
+
+    # This works, but it is not optimal
+    table_name = 'my_table'
+    cur.execute(
+        "insert into %s values (%%s, %%s)" % table_name,
+        [10, 20])
+
+This sort of works, but it is an accident waiting to happen: the table name
+may be an invalid SQL literal and need quoting; even more serious is the
+security problem in case the table name comes from an untrusted source. The
+name should be escaped using `~psycopg3.pq.Escaping.escape_identifier()`::
+
+    from psycopg3.pq import Escaping
+
+    # This works, but it is not optimal
+    table_name = 'my_table'
+    cur.execute(
+        "insert into %s values (%%s, %%s)" % Escaping.escape_identifier(table_name),
+        [10, 20])
+
+This is now safe, but it somewhat ad-hoc. In case, for some reason, it is
+necessary to include a value in the query string (as opposite as in a value)
+the merging rule is still different. It is also still relatively dangerous: if
+`!escape_identifier()` is forgotten somewhere, the program will usually work,
+but will eventually crash in the presence of a table or field name with
+containing characters to escape, or will present a potentially exploitable
+weakness.
+
+The objects exposed by the `!psycopg3.sql` module allow generating SQL
+statements on the fly, separating clearly the variable parts of the statement
+from the query parameters::
+
+    from psycopg3 import sql
+
+    cur.execute(
+        sql.SQL("insert into {} values (%s, %s)")
+            .format(sql.Identifier('my_table')),
+        [10, 20])
+
+
+Module usage
+------------
+
+Usually you should express the template of your query as an `SQL` instance
+with ``{}``\-style placeholders and use `~SQL.format()` to merge the variable
+parts into them, all of which must be `Composable` subclasses. You can still
+have ``%s``\-style placeholders in your query and pass values to
+`~psycopg3.Cursor.execute()`: such value placeholders will be untouched by
+`!format()`::
+
+    query = sql.SQL("select {field} from {table} where {pkey} = %s").format(
+        field=sql.Identifier('my_name'),
+        table=sql.Identifier('some_table'),
+        pkey=sql.Identifier('id'))
+
+The resulting object is meant to be passed directly to cursor methods such as
+`~psycopg3.Cursor.execute()`, `~psycopg3.Cursor.executemany()`,
+`~psycopg3.Cursor.copy()`, but can also be used to compose a query as a Python
+string, using the `~Composable.as_string()` method::
+
+    cur.execute(query, (42,))
+    full_query = query.as_string(cur)
+
+If part of your query is a variable sequence of arguments, such as a
+comma-separated list of field names, you can use the `SQL.join()` method to
+pass them to the query::
+
+    query = sql.SQL("select {fields} from {table}").format(
+        fields=sql.SQL(',').join([
+            sql.Identifier('field1'),
+            sql.Identifier('field2'),
+            sql.Identifier('field3'),
+        ]),
+        table=sql.Identifier('some_table'))
+
+
+`!sql` objects
+--------------
+
+The `!sql` objects are in the following inheritance hierarchy:
+
+|   `Composable`: the base class exposing the common interface
+|   ``|__`` `SQL`: a literal snippet of an SQL query
+|   ``|__`` `Identifier`: a PostgreSQL identifier or dot-separated sequence of identifiers
+|   ``|__`` `Literal`: a value hardcoded into a query
+|   ``|__`` `Placeholder`: a `%s`\ -style placeholder whose value will be added later e.g. by `~psycopg3.Cursor.execute()`
+|   ``|__`` `Composed`: a sequence of `!Composable` instances.
+
+
+.. autoclass:: Composable()
+
+    .. automethod:: as_string
+
+
+.. autoclass:: SQL
+
+    .. automethod:: format
+
+    .. automethod:: join
+
+
+.. autoclass:: Identifier
+
+.. autoclass:: Literal
+
+.. autoclass:: Placeholder
+
+.. autoclass:: Composed
+
+    .. automethod:: join
+
+
+Utility functions
+-----------------
+
+.. autofunction:: quote
+
+.. data::
+    NULL
+    DEFAULT
+
+    `sql.SQL` objects often useful in queries.
index 0aa0948dfdbfa52fa0f501e427f724957ece24d4..249830ae82ce56323afdfc10d64659cf97ccb95d 100644 (file)
@@ -31,9 +31,9 @@ class Composable(object):
     """
     Abstract base class for objects that can be used to compose an SQL string.
 
-    `!Composable` objects can be passed directly to `~cursor.execute()`,
-    `~cursor.executemany()`, `~cursor.copy_expert()` in place of the query
-    string.
+    `!Composable` objects can be passed directly to
+    `~psycopg3.Cursor.execute()`, `~psycopg3.Cursor.executemany()`,
+    `~psycopg3.Cursor.copy()` in place of the query string.
 
     `!Composable` objects can be joined using the ``+`` operator: the result
     will be a `Composed` instance containing the objects joined. The operator
@@ -55,9 +55,10 @@ class Composable(object):
         :param context: the context to evaluate the string into.
         :type context: `connection` or `cursor`
 
-        The method is automatically invoked by `~cursor.execute()`,
-        `~cursor.executemany()`, `~cursor.copy_expert()` if a `!Composable` is
-        passed instead of the query string.
+        The method is automatically invoked by `~psycopg3.Cursor.execute()`,
+        `~psycopg3.Cursor.executemany()`, `~psycopg3.Cursor.copy()` if a
+        `!Composable` is passed instead of the query string.
+
         """
         raise NotImplementedError
 
@@ -202,7 +203,7 @@ class SQL(Composable):
 
         If a `!Composable` objects is passed to the template it will be merged
         according to its `as_string()` method. If any other Python object is
-        passed, it will be wrapped in a `Literal` object and so escacaped
+        passed, it will be wrapped in a `Literal` object and so escaped
         according to SQL rules.
 
         Example::