From: Gord Thompson Date: Thu, 2 Apr 2020 17:51:11 +0000 (-0600) Subject: Broaden is[not]_distinct_from support X-Git-Tag: rel_1_3_16~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0b8b4bcdd5fea10fdf4fc9c89db809381c1b8fe3;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Broaden is[not]_distinct_from support Added support for .is[not]_distinct_from to SQL Server, MySQL, and Oracle. Fixes: #5137 Change-Id: I3b4d3b199821a55687f83c9a5b63a95d07a64cd5 (cherry picked from commit 207e0b2fc0b36acca398b163c698412deec7077e) --- diff --git a/doc/build/changelog/unreleased_13/5137.rst b/doc/build/changelog/unreleased_13/5137.rst new file mode 100644 index 0000000000..c53ca01987 --- /dev/null +++ b/doc/build/changelog/unreleased_13/5137.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: mssql, mysql, oracle, usecase + :tickets: 5137 + + Added support for :meth:`.ColumnOperators.is_distinct_from` and + :meth:`.ColumnOperators.isnot_distinct_from` to SQL Server, + MySQL, and Oracle. diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index b03300a6ff..6e2b2f94c7 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -1868,6 +1868,18 @@ class MSSQLCompiler(compiler.SQLCompiler): def visit_empty_set_expr(self, type_): return "SELECT 1 WHERE 1!=1" + def visit_is_distinct_from_binary(self, binary, operator, **kw): + return "NOT EXISTS (SELECT %s INTERSECT SELECT %s)" % ( + self.process(binary.left), + self.process(binary.right), + ) + + def visit_isnot_distinct_from_binary(self, binary, operator, **kw): + return "EXISTS (SELECT %s INTERSECT SELECT %s)" % ( + self.process(binary.left), + self.process(binary.right), + ) + class MSSQLStrictCompiler(MSSQLCompiler): diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 82a5a6b220..0d4c92410c 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1551,6 +1551,18 @@ class MySQLCompiler(compiler.SQLCompiler): } ) + def visit_is_distinct_from_binary(self, binary, operator, **kw): + return "NOT (%s <=> %s)" % ( + self.process(binary.left), + self.process(binary.right), + ) + + def visit_isnot_distinct_from_binary(self, binary, operator, **kw): + return "%s <=> %s" % ( + self.process(binary.left), + self.process(binary.right), + ) + class MySQLDDLCompiler(compiler.DDLCompiler): def get_column_specification(self, column, **kw): diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 312823b004..d565fd7f0c 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -1123,6 +1123,18 @@ class OracleCompiler(compiler.SQLCompiler): return tmp + def visit_is_distinct_from_binary(self, binary, operator, **kw): + return "DECODE(%s, %s, 0, 1) = 1" % ( + self.process(binary.left), + self.process(binary.right), + ) + + def visit_isnot_distinct_from_binary(self, binary, operator, **kw): + return "DECODE(%s, %s, 0, 1) = 0" % ( + self.process(binary.left), + self.process(binary.right), + ) + class OracleDDLCompiler(compiler.DDLCompiler): def define_constraint_cascades(self, constraint): diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index dda2161e2f..3e8d2aaeb4 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -1450,9 +1450,6 @@ class SQLiteDialect(default.DefaultDialect): colspecs = colspecs isolation_level = None - supports_cast = True - supports_default_values = True - construct_arguments = [ (sa_schema.Table, {"autoincrement": False}), (sa_schema.Index, {"where": None}), diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 62ceb5ccc8..dfbebbc7fb 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -129,6 +129,8 @@ class DefaultDialect(interfaces.Dialect): supports_empty_insert = True supports_multivalues_insert = False + supports_is_distinct_from = True + supports_server_side_cursors = False server_version_info = None diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 7a1f532a66..8720dbae6d 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -1100,3 +1100,19 @@ class SuiteRequirements(Requirements): """If persistence information is returned by the reflection of computed columns""" return exclusions.closed() + + @property + def supports_is_distinct_from(self): + """Supports some form of "x IS [NOT] DISTINCT FROM y" construct. + Different dialects will implement their own flavour, e.g., + sqlite will emit "x IS NOT y" instead of "x IS DISTINCT FROM y". + + .. seealso:: + + :meth:`.ColumnOperators.is_distinct_from` + + """ + return exclusions.skip_if( + lambda config: not config.db.dialect.supports_is_distinct_from, + "driver doesn't support an IS DISTINCT FROM construct", + ) diff --git a/lib/sqlalchemy/testing/suite/test_select.py b/lib/sqlalchemy/testing/suite/test_select.py index 1253ca81cd..bd46b95e91 100644 --- a/lib/sqlalchemy/testing/suite/test_select.py +++ b/lib/sqlalchemy/testing/suite/test_select.py @@ -702,3 +702,52 @@ class ComputedColumnTest(fixtures.TablesTest): .order_by(self.tables.square.c.id) ).fetchall() eq_(res, [(100, 40), (1764, 168)]) + + +class IsOrIsNotDistinctFromTest(fixtures.TablesTest): + __backend__ = True + __requires__ = ("supports_is_distinct_from",) + + @testing.provide_metadata + @testing.combinations( + ("both_int_different", 0, 1, 1), + ("both_int_same", 1, 1, 0), + ("one_null_first", None, 1, 1), + ("one_null_second", 0, None, 1), + ("both_null", None, None, 0), + id_="iaaa", + argnames="col_a_value, col_b_value, expected_row_count_for_is", + ) + def test_is_or_isnot_distinct_from( + self, col_a_value, col_b_value, expected_row_count_for_is, connection + ): + meta = self.metadata + tbl = Table( + "is_distinct_test", + meta, + Column("id", Integer, primary_key=True), + Column("col_a", Integer, nullable=True), + Column("col_b", Integer, nullable=True), + ) + tbl.create(connection) + connection.execute( + tbl.insert(), + [{"id": 1, "col_a": col_a_value, "col_b": col_b_value}], + ) + + result = connection.execute( + tbl.select(tbl.c.col_a.is_distinct_from(tbl.c.col_b)) + ).fetchall() + eq_( + len(result), expected_row_count_for_is, + ) + + expected_row_count_for_isnot = ( + 1 if expected_row_count_for_is == 0 else 0 + ) + result = connection.execute( + tbl.select(tbl.c.col_a.isnot_distinct_from(tbl.c.col_b)) + ).fetchall() + eq_( + len(result), expected_row_count_for_isnot, + )