]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add a new FAQ section explaining how to ensure parenthesis
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 25 Jan 2018 21:11:29 +0000 (16:11 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 25 Jan 2018 21:13:56 +0000 (16:13 -0500)
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)

doc/build/faq/sqlexpressions.rst

index 604a138ebcff611ee4e90893b0387c6d478be08a..e81455602cb0330f524d20146e2e63a86be76ddb 100644 (file)
@@ -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.
+