--- /dev/null
+.. change::
+ :tags: usecase, sql
+ :tickets: 8780
+
+ Added a full suite of new SQL bitwise operators, for performing
+ database-side bitwise expressions on appropriate data values such as
+ integers, bit-strings, and similar. Pull request courtesy Yegor Statkevich.
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
..
+.. _operators_bitwise:
+
+Bitwise Operators
+^^^^^^^^^^^^^^^^^
+
+Bitwise operator functions provide uniform access to bitwise operators across
+different backends, which are expected to operate on compatible
+values such as integers and bit-strings (e.g. PostgreSQL
+:class:`_postgresql.BIT` and similar). Note that these are **not** general
+boolean operators.
+
+.. versionadded:: 2.0.2 Added dedicated operators for bitwise operations.
+
+* :meth:`_sql.ColumnOperators.bitwise_not`, :func:`_sql.bitwise_not`.
+ Available as a column-level method, producing a bitwise NOT clause against a
+ parent object::
+
+ >>> print(column("x").bitwise_not())
+ ~x
+
+ This operator is also available as a column-expression-level method, applying
+ bitwise NOT to an individual column expression::
+
+ >>> from sqlalchemy import bitwise_not
+ >>> print(bitwise_not(column("x")))
+ ~x
+
+ ..
+
+* :meth:`_sql.ColumnOperators.bitwise_and` produces bitwise AND::
+
+ >>> print(column("x").bitwise_and(5))
+ x & :x_1
+
+ ..
+
+* :meth:`_sql.ColumnOperators.bitwise_or` produces bitwise OR::
+
+ >>> print(column("x").bitwise_or(5))
+ x | :x_1
+
+ ..
+
+* :meth:`_sql.ColumnOperators.bitwise_xor` produces bitwise XOR::
+
+ >>> print(column("x").bitwise_xor(5))
+ x ^ :x_1
+
+ For PostgreSQL dialects, "#" is used to represent bitwise XOR; this emits
+ automatically when using one of these backends::
+
+ >>> from sqlalchemy.dialects import postgresql
+ >>> print(column("x").bitwise_xor(5).compile(dialect=postgresql.dialect()))
+ x # %(x_1)s
+
+ ..
+
+* :meth:`_sql.ColumnOperators.bitwise_rshift`, :meth:`_sql.ColumnOperators.bitwise_lshift`
+ produce bitwise shift operators::
+
+ >>> print(column("x").bitwise_rshift(5))
+ x >> :x_1
+ >>> print(column("x").bitwise_lshift(5))
+ x << :x_1
+
+ ..
+
Using Conjunctions and Negations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autofunction:: bindparam
+.. autofunction:: bitwise_not
+
.. autofunction:: case
.. autofunction:: cast
from .sql.expression import BinaryExpression as BinaryExpression
from .sql.expression import bindparam as bindparam
from .sql.expression import BindParameter as BindParameter
+from .sql.expression import bitwise_not as bitwise_not
from .sql.expression import BooleanClauseList as BooleanClauseList
from .sql.expression import CacheKey as CacheKey
from .sql.expression import Case as Case
self.process(element.stop, **kw),
)
+ def visit_bitwise_xor_op_binary(self, binary, operator, **kw):
+ return self._generate_generic_binary(binary, " # ", **kw)
+
def visit_json_getitem_op_binary(
self, binary, operator, _cast_applied=False, **kw
):
return UnaryExpression._create_distinct(expr)
+def bitwise_not(expr: _ColumnExpressionArgument[_T]) -> UnaryExpression[_T]:
+ """Produce a unary bitwise NOT clause, typically via the ``~`` operator.
+
+ Not to be confused with boolean negation :func:`_sql.not_`.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+
+ """
+
+ return UnaryExpression._create_bitwise_not(expr)
+
+
def extract(field: str, expr: _ColumnExpressionArgument[Any]) -> Extract:
"""Return a :class:`.Extract` construct.
operators.asc_op: " ASC",
operators.nulls_first_op: " NULLS FIRST",
operators.nulls_last_op: " NULLS LAST",
+ # bitwise
+ operators.bitwise_xor_op: " ^ ",
+ operators.bitwise_or_op: " | ",
+ operators.bitwise_and_op: " & ",
+ operators.bitwise_not_op: "~",
+ operators.bitwise_lshift_op: " << ",
+ operators.bitwise_rshift_op: " >> ",
}
FUNCTIONS: Dict[Type[Function[Any]], str] = {
return UnaryExpression(expr, operator=operators.neg, type_=expr.type)
+def _bitwise_not_impl(
+ expr: ColumnElement[Any], op: OperatorType, **kw: Any
+) -> ColumnElement[Any]:
+ """See :meth:`.ColumnOperators.bitwise_not`."""
+
+ return UnaryExpression(
+ expr, operator=operators.bitwise_not_op, type_=expr.type
+ )
+
+
def _match_impl(
expr: ColumnElement[Any], op: OperatorType, other: Any, **kw: Any
) -> ColumnElement[Any]:
"sub": (_binary_operate, util.EMPTY_DICT),
"div": (_binary_operate, util.EMPTY_DICT),
"mod": (_binary_operate, util.EMPTY_DICT),
+ "bitwise_xor_op": (_binary_operate, util.EMPTY_DICT),
+ "bitwise_or_op": (_binary_operate, util.EMPTY_DICT),
+ "bitwise_and_op": (_binary_operate, util.EMPTY_DICT),
+ "bitwise_not_op": (_bitwise_not_impl, util.EMPTY_DICT),
+ "bitwise_lshift_op": (_binary_operate, util.EMPTY_DICT),
+ "bitwise_rshift_op": (_binary_operate, util.EMPTY_DICT),
"truediv": (_binary_operate, util.EMPTY_DICT),
"floordiv": (_binary_operate, util.EMPTY_DICT),
"custom_op": (_custom_op_operate, util.EMPTY_DICT),
wraps_column_expression=False,
)
+ @classmethod
+ def _create_bitwise_not(
+ cls,
+ expr: _ColumnExpressionArgument[_T],
+ ) -> UnaryExpression[_T]:
+ col_expr: ColumnElement[_T] = coercions.expect(
+ roles.ExpressionElementRole, expr
+ )
+ return UnaryExpression(
+ col_expr,
+ operator=operators.bitwise_not_op,
+ type_=col_expr.type,
+ wraps_column_expression=False,
+ )
+
@property
def _order_by_label_element(self) -> Optional[Label[Any]]:
if self.modifier in (operators.desc_op, operators.asc_op):
from ._elements_constructors import asc as asc
from ._elements_constructors import between as between
from ._elements_constructors import bindparam as bindparam
+from ._elements_constructors import bitwise_not as bitwise_not
from ._elements_constructors import case as case
from ._elements_constructors import cast as cast
from ._elements_constructors import collate as collate
"""
return self.operate(ilike_op, other, escape=escape)
+ def bitwise_xor(self, other: Any) -> ColumnOperators:
+ """Produce a bitwise XOR operation, typically via the ``^``
+ operator, or ``#`` for PostgreSQL.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_xor_op, other)
+
+ def bitwise_or(self, other: Any) -> ColumnOperators:
+ """Produce a bitwise OR operation, typically via the ``|``
+ operator.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_or_op, other)
+
+ def bitwise_and(self, other: Any) -> ColumnOperators:
+ """Produce a bitwise AND operation, typically via the ``&``
+ operator.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_and_op, other)
+
+ def bitwise_not(self) -> ColumnOperators:
+ """Produce a bitwise NOT operation, typically via the ``~``
+ operator.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_not_op)
+
+ def bitwise_lshift(self, other: Any) -> ColumnOperators:
+ """Produce a bitwise LSHIFT operation, typically via the ``<<``
+ operator.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_lshift_op, other)
+
+ def bitwise_rshift(self, other: Any) -> ColumnOperators:
+ """Produce a bitwise RSHIFT operation, typically via the ``>>``
+ operator.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`operators_bitwise`
+
+ """
+
+ return self.operate(bitwise_rshift_op, other)
+
def in_(self, other: Any) -> ColumnOperators:
"""Implement the ``in`` operator.
raise NotImplementedError()
+@_operator_fn
+def bitwise_xor_op(a: Any, b: Any) -> Any:
+ return a.bitwise_xor(b)
+
+
+@_operator_fn
+def bitwise_or_op(a: Any, b: Any) -> Any:
+ return a.bitwise_or(b)
+
+
+@_operator_fn
+def bitwise_and_op(a: Any, b: Any) -> Any:
+ return a.bitwise_and(b)
+
+
+@_operator_fn
+def bitwise_not_op(a: Any) -> Any:
+ return a.bitwise_not()
+
+
+@_operator_fn
+def bitwise_lshift_op(a: Any, b: Any) -> Any:
+ return a.bitwise_lshift(b)
+
+
+@_operator_fn
+def bitwise_rshift_op(a: Any, b: Any) -> Any:
+ return a.bitwise_rshift(b)
+
+
def is_comparison(op: OperatorType) -> bool:
return op in _comparison or isinstance(op, custom_op) and op.is_comparison
floordiv: 8,
mod: 8,
neg: 8,
+ bitwise_not_op: 8,
add: 7,
sub: 7,
+ bitwise_xor_op: 7,
+ bitwise_or_op: 7,
+ bitwise_and_op: 7,
+ bitwise_lshift_op: 7,
+ bitwise_rshift_op: 7,
concat_op: 6,
filter_op: 6,
match_op: 5,
"WHERE usages.date <@ %(date_1)s::DATERANGE",
)
+ def test_bitwise_xor(self):
+ c1 = column("c1", Integer)
+ c2 = column("c2", Integer)
+ self.assert_compile(
+ select(c1.bitwise_xor(c2)),
+ "SELECT c1 # c2 AS anon_1",
+ )
+
class InsertOnConflictTest(fixtures.TablesTest, AssertsCompiledSQL):
__dialect__ = postgresql.dialect()
self.assert_compile(op2, "mytable.myid hoho :myid_1 lala :param_1")
self.assert_compile(op3, "(mytable.myid hoho :myid_1) lala :param_1")
+ def test_bitwise_not_precedence(self):
+ op1 = operators.bitwise_not_op
+ c = self.table1.c.myid
+ op2 = op1(c).op("lala", precedence=7)(4)
+ op3 = op1(c).op("lala", precedence=9)(4)
+
+ self.assert_compile(op2, "~mytable.myid lala :param_1")
+ self.assert_compile(op3, "(~mytable.myid) lala :param_1")
+
+ @testing.combinations(
+ ("xor", operators.bitwise_xor_op, "^"),
+ ("or", operators.bitwise_or_op, "|"),
+ ("and", operators.bitwise_and_op, "&"),
+ ("lshift", operators.bitwise_lshift_op, "<<"),
+ ("rshift", operators.bitwise_rshift_op, ">>"),
+ id_="iaa",
+ )
+ def test_bitwise_op_precedence(self, py_op, sql_op):
+ c = self.table1.c.myid
+ op1 = py_op(c, 5).op("lala", precedence=6)(4)
+ op2 = py_op(c, 5).op("lala", precedence=8)(4)
+
+ self.assert_compile(
+ op1, f"mytable.myid {sql_op} :myid_1 lala :param_1"
+ )
+ self.assert_compile(
+ op2, f"(mytable.myid {sql_op} :myid_1) lala :param_1"
+ )
+
def test_is_eq_precedence_flat(self):
self.assert_compile(
(self.table1.c.name == null())
r"use the .scalar_values\(\) method.",
):
fn(values(t.c.data).data([(1,), (42,)]))
+
+
+class BitOpTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+ __dialect__ = "default"
+
+ def test_compile_not_column_lvl(self):
+ c = column("c", Integer)
+
+ self.assert_compile(
+ select(c.bitwise_not()),
+ "SELECT ~c",
+ )
+
+ def test_compile_not_colexpr_lvl(self):
+ c = column("c", Integer)
+
+ self.assert_compile(
+ select(operators.bitwise_not_op(c)),
+ "SELECT ~c",
+ )
+
+ @testing.combinations(
+ ("xor", operators.bitwise_xor_op, "^"),
+ ("xor_lambda", lambda c1, c2: c1.bitwise_xor(c2), "^"),
+ ("or", operators.bitwise_or_op, "|"),
+ ("or_lambda", lambda c1, c2: c1.bitwise_or(c2), "|"),
+ ("and", operators.bitwise_and_op, "&"),
+ ("and_lambda", lambda c1, c2: c1.bitwise_and(c2), "&"),
+ ("lshift", operators.bitwise_lshift_op, "<<"),
+ ("ls_lambda", lambda c1, c2: c1.bitwise_lshift(c2), "<<"),
+ ("rshift", operators.bitwise_rshift_op, ">>"),
+ ("rs_lambda", lambda c1, c2: c1.bitwise_rshift(c2), ">>"),
+ id_="iaa",
+ )
+ def test_compile_binary(self, py_op, sql_op):
+ c1 = column("c1", Integer)
+ c2 = column("c2", Integer)
+
+ self.assert_compile(
+ select(py_op(c1, c2)),
+ f"SELECT c1 {sql_op} c2 AS anon_1",
+ )