From: Mike Bayer Date: Fri, 5 Apr 2019 01:43:12 +0000 (-0400) Subject: Enhance documentation for string compilation use cases X-Git-Tag: rel_1_3_3~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6845be0927245e47c27f8e160472cf9a55a41dc4;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Enhance documentation for string compilation use cases - 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 --- diff --git a/doc/build/core/internals.rst b/doc/build/core/internals.rst index 56b6162e9f..fd2ac783ac 100644 --- a/doc/build/core/internals.rst +++ b/doc/build/core/internals.rst @@ -40,4 +40,7 @@ Some key internal constructs are listed here. .. autoclass:: sqlalchemy.sql.compiler.SQLCompiler :members: +.. autoclass:: sqlalchemy.sql.compiler.StrSQLCompiler + :members: + diff --git a/doc/build/errors.rst b/doc/build/errors.rst index 994638c0d4..8c9554f9c4 100644 --- a/doc/build/errors.rst +++ b/doc/build/errors.rst @@ -318,6 +318,77 @@ the database driver (DBAPI), not SQLAlchemy itself. SQL Expression Language ======================= +.. _error_l7de: + +Compiler StrSQLCompiler can't render element of 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" `_ 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 + can't render element of type + + +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: not supported between instances of 'ColumnProperty' and ----------------------------------------------------------------------------------------- diff --git a/doc/build/faq/sqlexpressions.rst b/doc/build/faq/sqlexpressions.rst index 2571c431c8..6a52e747df 100644 --- a/doc/build/faq/sqlexpressions.rst +++ b/doc/build/faq/sqlexpressions.rst @@ -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` diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 5ce2d5df22..1e575626a5 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -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" diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 15ddd7d6fb..c7fe3dc50e 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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` """