From: Mike Bayer Date: Mon, 27 Aug 2018 18:51:59 +0000 (-0400) Subject: document expanding bound parameters, in_(), and baked query use cases X-Git-Tag: rel_1_3_0b1~90 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=90a772b19339b97517bc46f016122341528e440a;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git document expanding bound parameters, in_(), and baked query use cases Change-Id: I09ccd73ebabbea4b5098ba5b170786ac065444c8 --- diff --git a/doc/build/orm/extensions/baked.rst b/doc/build/orm/extensions/baked.rst index 593293b460..83faf299e7 100644 --- a/doc/build/orm/extensions/baked.rst +++ b/doc/build/orm/extensions/baked.rst @@ -332,6 +332,66 @@ to arrive at the current "baked" approach. Starting from the management, removal of all redundant Python execution, and queries built up with conditionals needed to be addressed, leading to the final approach. +Special Query Techniques +------------------------ + +This section will describe some techniques for specific query situations. + +.. _baked_in: + +Using IN expressions +^^^^^^^^^^^^^^^^^^^^ + +The :meth:`.ColumnOperators.in_` method in SQLAlchemy historically renders +a variable set of bound parameters based on the list of items that's passed +to the method. This doesn't work for baked queries as the length of that +list can change on different calls. To solve this problem, the +:paramref:`.bindparam.expanding` parameter supports a late-rendered IN +expression that is safe to be cached inside of baked query. The actual list +of elements is rendered at statement execution time, rather than at +statement compilation time:: + + bakery = baked.bakery() + + baked_query = bakery(lambda session: session.query(User)) + baked_query += lambda q: q.filter( + User.name.in_(bindparam('username', expanding=True))) + + result = baked_query.with_session(session).params( + username=['ed', 'fred']).all() + +.. seealso:: + + :paramref:`.bindparam.expanding` + + :meth:`.ColumnOperators.in_` + +Using Subqueries +^^^^^^^^^^^^^^^^ + +When using :class:`.Query` objects, it is often needed that one :class:`.Query` +object is used to generate a subquery within another. In the case where the +:class:`.Query` is currently in baked form, an interim method may be used to +retrieve the :class:`.Query` object, using the semi-private ``_as_query()`` +method. This method requires that a :class:`.Session` is passed, which should +be relative to the :class:`.Session` that is in the lambda callable, e.g. +``q.session`` or ``s``:: + + bakery = baked.bakery() + + my_subq = bakery(lambda s: s.query(User.id)) + my_subq += lambda q: q.filter(User.id == Address.user_id) + + # select a correlated subquery in the top columns list + my_q = bakery( + lambda s: s.query(Address.id, my_subq._as_query(s).as_scalar())) + + # use a correlated subquery in some of the criteria + my_q += lambda q: q.filter(my_subq._as_query(q.session).exists()) + +A future feature will provide a public method for the above use case. + + Disabling Baked Queries Session-wide ------------------------------------ diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index d79ba05af6..dd16b68628 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -1064,6 +1064,12 @@ class BindParameter(ColumnElement): This is to allow statement caching to be used in conjunction with an IN clause. + .. seealso:: + + :meth:`.ColumnOperators.in_` + + :ref:`baked_in` - with baked queries + .. note:: The "expanding" feature does not support "executemany"- style parameter sets. diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index a9f4e3e3ed..5b4a28a06d 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -494,20 +494,84 @@ class ColumnOperators(Operators): def in_(self, other): """Implement the ``in`` operator. - In a column context, produces the clause ``a IN other``. - "other" may be a tuple/list of column expressions, - or a :func:`~.expression.select` construct. + In a column context, produces the clause ``column IN ``. - In the case that ``other`` is an empty sequence, the compiler - produces an "empty in" expression. This defaults to the - expression "1 != 1" to produce false in all cases. The - :paramref:`.create_engine.empty_in_strategy` may be used to - alter this behavior. + The given parameter ``other`` may be: - .. versionchanged:: 1.2 The :meth:`.ColumnOperators.in_` and - :meth:`.ColumnOperators.notin_` operators - now produce a "static" expression for an empty IN sequence - by default. + * A list of literal values, e.g.:: + + stmt.where(column.in_([1, 2, 3])) + + In this calling form, the list of items is converted to a set of + bound parameters the same length as the list given:: + + WHERE COL IN (?, ?, ?) + + * An empty list, e.g.:: + + stmt.where(column.in_([])) + + In this calling form, the expression renders a "false" expression, + e.g.:: + + WHERE 1 != 1 + + This "false" expression has historically had different behaviors + in older SQLAlchemy versions, see + :paramref:`.create_engine.empty_in_strategy` for behavioral options. + + .. versionchanged:: 1.2 simplified the behavior of "empty in" + expressions + + * A bound parameter, e.g. :func:`.bindparam`, may be used if it + includes the :paramref:`.bindparam.expanding` flag:: + + stmt.where(column.in_(bindparam('value', expanding=True))) + + In this calling form, the expression renders a special non-SQL + placeholder expression that looks like:: + + WHERE COL IN ([EXPANDING_value]) + + This placeholder expression is intercepted at statement execution + time to be converted into the variable number of bound parameter + form illustrated earlier. If the statement were executed as:: + + connection.execute(stmt, {"value": [1, 2, 3]}) + + The database would be passed a bound parameter for each value:: + + WHERE COL IN (?, ?, ?) + + .. versionadded:: 1.2 added "expanding" bound parameters + + If an empty list is passed, a special "empty list" expression, + which is specific to the database in use, is rendered. On + SQLite this would be:: + + WHERE COL IN (SELECT 1 FROM (SELECT 1) WHERE 1!=1) + + .. versionadded:: 1.3 "expanding" bound parameters now support + empty lists + + * a :func:`.select` construct, which is usually a correlated + scalar select:: + + stmt.where( + column.in_( + select([othertable.c.y]). + where(table.c.x == othertable.c.x) + ) + ) + + In this calling form, :meth:`.ColumnOperators.in_` renders as given:: + + WHERE COL IN (SELECT othertable.y + FROM othertable WHERE othertable.x = table.x) + + :param other: a list of literals, a :func:`.select` construct, + or a :func:`.bindparam` construct that includes the + :paramref:`.bindparam.expanding` flag set to True. """ return self.operate(in_op, other)