From: Mike Bayer Date: Thu, 25 Jan 2018 21:11:29 +0000 (-0500) Subject: Add a new FAQ section explaining how to ensure parenthesis X-Git-Tag: rel_1_1_16~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6ae587b59f38554e2bafcce490053925c40823b0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add a new FAQ section explaining how to ensure parenthesis with op(). Not sure if this can be further improved at some point, such as if a BinaryExpression automatically applied self_group() when op() is called, however for the moment op() behaves consistently as with all other operators without any ad-hoc effects. Change-Id: Ia7f1ab43990450bd96757848b77a35e8fedeab63 References: #4174 (cherry picked from commit 1d435b9f49136f53aac4aa457f7744227be0b26a) --- diff --git a/doc/build/faq/sqlexpressions.rst b/doc/build/faq/sqlexpressions.rst index 604a138ebc..e81455602c 100644 --- a/doc/build/faq/sqlexpressions.rst +++ b/doc/build/faq/sqlexpressions.rst @@ -138,3 +138,98 @@ of zero length. Instead, don't emit the Query in the first place, if no rows should be returned. The warning is best promoted to a full error condition using the Python warnings filter (see http://docs.python.org/library/warnings.html). +.. _faq_sql_expression_op_parenthesis: + +I'm using op() to generate a custom operator and my parenthesis are not coming out correctly +--------------------------------------------------------------------------------------------- + +The :meth:`.Operators.op` method allows one to create a custom database operator +otherwise not known by SQLAlchemy:: + + >>> print(column('q').op('->')(column('p'))) + q -> p + +However, when using it on the right side of a compound expression, it doesn't +generate parenthesis as we expect:: + + >>> print((column('q1') + column('q2')).op('->')(column('p'))) + q1 + q2 -> p + +Where above, we probably want ``(q1 + q2) -> p``. + +The solution to this case is to set the precedence of the operator, using +the :paramref:`.Operators.op.precedence` parameter, to a high +number, where 100 is the maximum value, and the highest number used by any +SQLAlchemy operator is currently 15:: + + >>> print((column('q1') + column('q2')).op('->', precedence=100)(column('p'))) + (q1 + q2) -> p + +We can also usually force parenthesization around a binary expression (e.g. +an expression that has left/right operands and an operator) using the +:meth:`.ColumnElement.self_group` method:: + + >>> print((column('q1') + column('q2')).self_group().op('->')(column('p'))) + (q1 + q2) -> p + +Why are the parentheses rules like this? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A lot of databases barf when there are excessive parenthesis or when +parenthesis are in unusual places they doesn't expect, so SQLAlchemy does not +generate parenthesis based on groupings, it uses operator precedence and if the +operator is known to be associative, so that parenthesis are generated +minimally. Otherwise, an expression like:: + + column('a') & column('b') & column('c') & column('d') + +would produce:: + + (((a AND b) AND c) AND d) + +which is fine but would probably annoy people (and be reported as a bug). In +other cases, it leads to things that are more likely to confuse databases or at +the very least readability, such as:: + + column('q', ARRAY(Integer, dimensions=2))[5][6] + +would produce:: + + ((q[5])[6]) + +There are also some edge cases where we get things like ``"(x) = 7"`` and databases +really don't like that either. So parenthesization doesn't naively +parenthesize, it uses operator precedence and associativity to determine +groupings. + +For :meth:`.Operators.op`, the value of precedence defaults to zero. + +What if we defaulted the value of :paramref:`.Operators.op.precedence` to 100, +e.g. the highest? Then this expression makes more parenthesis, but is +otherwise OK, that is, these two are equivalent:: + + >>> print (column('q') - column('y')).op('+', precedence=100)(column('z')) + (q - y) + z + >>> print (column('q') - column('y')).op('+')(column('z')) + q - y + z + +but these two are not:: + + >>> print column('q') - column('y').op('+', precedence=100)(column('z')) + q - y + z + >>> print column('q') - column('y').op('+')(column('z')) + q - (y + z) + +For now, it's not clear that as long as we are doing parenthesization based on +operator precedence and associativity, if there is really a way to parenthesize +automatically for a generic operator with no precedence given that is going to +work in all cases, because sometimes you want a custom op to have a lower +precedence than the other operators and sometimes you want it to be higher. + +It is possible that maybe if the "binary" expression above forced the use of +the ``self_group()`` method when ``op()`` is called, making the assumption that +a compound expression on the left side can always be parenthesized harmlessly. +Perhaps this change can be made at some point, however for the time being +keeping the parenthesization rules more internally consistent seems to be +the safer approach. +