]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Enhance documentation for string compilation use cases
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 5 Apr 2019 01:43:12 +0000 (21:43 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 5 Apr 2019 01:44:49 +0000 (21:44 -0400)
- Add a web link for UnsupportedCompilationError
- Add new section to errors.rst
- add more detail and cross-linking to the FAQ
- include security caveats for parameter rendering

Fixes: #4595
Change-Id: I31ea57c18d65770cd2a51276bbe2847a9eb72bba

doc/build/core/internals.rst
doc/build/errors.rst
doc/build/faq/sqlexpressions.rst
lib/sqlalchemy/exc.py
lib/sqlalchemy/sql/compiler.py

index 56b6162e9f750275084886ab6ba63bba667d3f96..fd2ac783ac14b1ee16471259687778410cb6f2ac 100644 (file)
@@ -40,4 +40,7 @@ Some key internal constructs are listed here.
 .. autoclass:: sqlalchemy.sql.compiler.SQLCompiler
     :members:
 
+.. autoclass:: sqlalchemy.sql.compiler.StrSQLCompiler
+    :members:
+
 
index 994638c0d48a96cbccd4510675e2e8194f3d1716..8c9554f9c4760682ee91b8a2c6e8d181ee465e2a 100644 (file)
@@ -318,6 +318,77 @@ the database driver (DBAPI), not SQLAlchemy itself.
 SQL Expression Language
 =======================
 
+.. _error_l7de:
+
+Compiler StrSQLCompiler can't render element of type <element type>
+-------------------------------------------------------------------
+
+This error usually occurs when attempting to stringify a SQL expression
+construct that includes elements which are not part of the default compilation;
+in this case, the error will be against the :class:`.StrSQLCompiler` class.
+In less common cases, it can also occur when the wrong kind of SQL expression
+is used with a particular type of database backend; in those cases, other
+kinds of SQL compiler classes will be named, such as ``SQLCompiler`` or
+``sqlalchemy.dialects.postgresql.PGCompiler``.  The guidance below is
+more specific to the "stringification" use case but describes the general
+background as well.
+
+Normally, a Core SQL construct or ORM :class:`.Query` object can be stringified
+directly, such as when we use ``print()``::
+
+  >>> from sqlalchemy import column
+  >>> print(column('x') == 5)
+  x = :x_1
+
+When the above SQL expression is stringified, the :class:`.StrSQLCompiler`
+compiler class is used, which is a special statement compiler that is invoked
+when a construct is stringified without any dialect-specific information.
+
+However, there are many constructs that are specific to some particular kind
+of database dialect, for which the :class:`.StrSQLCompiler` doesn't know how
+to turn into a string, such as the PostgreSQL
+`"insert on conflict" <postgresql_insert_on_conflict>`_ construct::
+
+  >>> from sqlalchemy.dialects.postgresql import insert
+  >>> from sqlalchemy import table, column
+  >>> my_table = table('my_table', column('x'), column('y'))
+  >>> insert_stmt = insert(my_table).values(x='foo')
+  >>> insert_stmt = insert_stmt.on_conflict_do_nothing(
+  ...     index_elements=['y']
+  ... )
+  >>> print(insert_stmt)
+  Traceback (most recent call last):
+
+  ...
+
+  sqlalchemy.exc.UnsupportedCompilationError:
+  Compiler <sqlalchemy.sql.compiler.StrSQLCompiler object at 0x7f04fc17e320>
+  can't render element of type
+  <class 'sqlalchemy.dialects.postgresql.dml.OnConflictDoNothing'>
+
+In order to stringify constructs that are specific to particular backend,
+the :meth:`.ClauseElement.compile` method must be used, passing either an
+:class:`.Engine` or a :class:`.Dialect` object which will invoke the correct
+compiler.   Below we use a PostgreSQL dialect::
+
+  >>> from sqlalchemy.dialects import postgresql
+  >>> print(insert_stmt.compile(dialect=postgresql.dialect()))
+  INSERT INTO my_table (x) VALUES (%(x)s) ON CONFLICT (y) DO NOTHING
+
+For an ORM :class:`.Query` object, the statement can be accessed using the
+:attr:`~.orm.query.Query.statement` accessor::
+
+    statement = query.statement
+    print(statement.compile(dialect=postgresql.dialect()))
+
+See the FAQ link below for additional detail on direct stringification /
+compilation of SQL elements.
+
+.. seealso::
+
+  :ref:`faq_sql_expression_string`
+
+
 TypeError: <operator> not supported between instances of 'ColumnProperty' and <something>
 -----------------------------------------------------------------------------------------
 
index 2571c431c8c68ffc43993072d4390a0431b340e3..6a52e747df5d3ebba06fecd0010a0980d17b5066 100644 (file)
@@ -11,19 +11,52 @@ SQL Expressions
 How do I render SQL expressions as strings, possibly with bound parameters inlined?
 ------------------------------------------------------------------------------------
 
-The "stringification" of a SQLAlchemy statement or Query in the vast majority
-of cases is as simple as::
-
-    print(str(statement))
-
-this applies both to an ORM :class:`~.orm.query.Query` as well as any :func:`.select` or other
-statement.   Additionally, to get the statement as compiled to a
-specific dialect or engine, if the statement itself is not already
-bound to one you can pass this in to :meth:`.ClauseElement.compile`::
-
-    print(statement.compile(someengine))
-
-or without an :class:`.Engine`::
+The "stringification" of a SQLAlchemy Core statement object or
+expression fragment, as well as that of an ORM :class:`.Query` object,
+in the majority of simple cases is as simple as using
+the ``str()`` builtin function, as below when use it with the ``print``
+function (note the Python ``print`` function also calls ``str()`` automatically
+if we don't use it explicitly)::
+
+    >>> from sqlalchemy import table, column, select
+    >>> t = table('my_table', column('x'))
+    >>> statement = select([t])
+    >>> print(str(statement))
+    SELECT my_table.x
+    FROM my_table
+
+The ``str()`` builtin, or an equivalent, can be invoked on ORM
+:class:`.Query`  object as well as any statement such as that of
+:func:`.select`, :func:`.insert` etc. and also any expression fragment, such
+as::
+
+    >>> from sqlalchemy import column
+    >>> print(column('x') == 'some value')
+    x = :x_1
+
+Stringifying for Specific Databases
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A complication arises when the statement or fragment we are stringifying
+contains elements that have a database-specific string format, or when it
+contains elements that are only available within a certain kind of database.
+In these cases, we might get a stringified statement that is not in the correct
+syntax for the database we are targeting, or the operation may raise a
+:class:`.UnsupportedCompilationError` exception.   In these cases, it is
+necessary that we stringify the statement using the
+:meth:`.ClauseElement.compile` method, while passing along an :class:`.Engine`
+or :class:`.Dialect` object that represents the target database.  Such as
+below, if we have a MySQL database engine, we can stringify a statement in
+terms of the MySQL dialect::
+
+    from sqlalchemy import create_engine
+
+    engine = create_engine("mysql+pymysql://scott:tiger@localhost/test")
+    print(statement.compile(engine))
+
+More directly, without building up an :class:`.Engine` object we can
+instantiate a :class:`.Dialect` object directly, as below where we
+use a PostgreSQL dialect::
 
     from sqlalchemy.dialects import postgresql
     print(statement.compile(dialect=postgresql.dialect()))
@@ -36,6 +69,16 @@ accessor first::
     statement = query.statement
     print(statement.compile(someengine))
 
+Rendering Bound Parameters Inline
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. warning:: **Never** use this technique with string content received from
+   untrusted input, such as from web forms or other user-input applications.
+   SQLAlchemy's facilities to  coerce Python values into direct SQL string
+   values are **not secure against untrusted input and do not validate the type
+   of data being passed**. Always use bound parameters when programmatically
+   invoking non-DDL SQL statements against a relational database.
+
 The above forms will render the SQL statement as it is passed to the Python
 :term:`DBAPI`, which includes that bound parameters are not rendered inline.
 SQLAlchemy normally does not stringify bound parameters, as this is handled
@@ -52,7 +95,7 @@ flag, passed to ``compile_kwargs``::
 
     s = select([t]).where(t.c.x == 5)
 
-    print(s.compile(compile_kwargs={"literal_binds": True}))
+    print(s.compile(compile_kwargs={"literal_binds": True}))  # **do not use** with untrusted input!!!
 
 the above approach has the caveats that it is only supported for basic
 types, such as ints and strings, and furthermore if a :func:`.bindparam`
index 5ce2d5df22a74e8d858a7604833cae957ca9e09b..1e575626a5061b9e41ec285beef581242dd05c0c 100644 (file)
@@ -154,8 +154,15 @@ class CompileError(SQLAlchemyError):
 class UnsupportedCompilationError(CompileError):
     """Raised when an operation is not supported by the given compiler.
 
+    .. seealso::
+
+        :ref:`faq_sql_expression_string`
+
+        :ref:`error_l7de`
     """
 
+    code = "l7de"
+
     def __init__(self, compiler, element_type):
         super(UnsupportedCompilationError, self).__init__(
             "Compiler %r can't render element of type %s"
index 15ddd7d6fb6c523c1247ea942fa80bac8b549498..c7fe3dc50e164bb664ac2a42f68b539d68c4a860 100644 (file)
@@ -2717,10 +2717,20 @@ class SQLCompiler(Compiled):
 
 
 class StrSQLCompiler(SQLCompiler):
-    """"a compiler subclass with a few non-standard SQL features allowed.
+    """A :class:`.SQLCompiler` subclass which allows a small selection
+    of non-standard SQL features to render into a string value.
 
-    Used for stringification of SQL statements when a real dialect is not
-    available.
+    The :class:`.StrSQLCompiler` is invoked whenever a Core expression
+    element is directly stringified without calling upon the
+    :meth:`.ClauseElement.compile` method.   It can render a limited set
+    of non-standard SQL constructs to assist in basic stringification,
+    however for more substantial custom or dialect-specific SQL constructs,
+    it will be necessary to make use of :meth:`.ClauseElement.compile`
+    directly.
+
+    .. seealso::
+
+        :ref:`faq_sql_expression_string`
 
     """