]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
resolve select to NULLTYPE if no columns
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 7 Mar 2023 14:03:07 +0000 (09:03 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 7 Mar 2023 14:03:07 +0000 (09:03 -0500)
Fixed regression where the :func:`_sql.select` construct would not be able
to render if it were given no columns and then used in the context of an
EXISTS, raising an internal exception instead. While an empty "SELECT" is
not typically valid SQL, in the context of EXISTS databases such as
PostgreSQL allow it, and in any case the condition now no longer raises
an internal exception.

For this case, also add an extra whitespace trim step for the unusual
case that there are no columns to render.  This is done in such a
way as to not interfere with other test cases that are involving
custom compilation schemes.

Fixes: #9440
Change-Id: If65ba9ce15d371f09b4342ad0669143b7b082a78

doc/build/changelog/unreleased_20/9440.rst [new file with mode: 0644]
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/selectable.py
test/sql/test_select.py

diff --git a/doc/build/changelog/unreleased_20/9440.rst b/doc/build/changelog/unreleased_20/9440.rst
new file mode 100644 (file)
index 0000000..aa2ecd7
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 9440
+
+    Fixed regression where the :func:`_sql.select` construct would not be able
+    to render if it were given no columns and then used in the context of an
+    EXISTS, raising an internal exception instead. While an empty "SELECT" is
+    not typically valid SQL, in the context of EXISTS databases such as
+    PostgreSQL allow it, and in any case the condition now no longer raises
+    an internal exception.
+
index bc463f9a1e7c8bb2f5dedae1504cfba786ddff25..ad0a3b686608b031587d17621c1bb99d17ac6787 100644 (file)
@@ -4668,6 +4668,16 @@ class SQLCompiler(Compiled):
             from_linter = None
             warn_linting = False
 
+        # adjust the whitespace for no inner columns, part of #9440,
+        # so that a no-col SELECT comes out as "SELECT WHERE..." or
+        # "SELECT FROM ...".
+        # while it would be better to have built the SELECT starting string
+        # without trailing whitespace first, then add whitespace only if inner
+        # cols were present, this breaks compatibility with various custom
+        # compilation schemes that are currently being tested.
+        if not inner_columns:
+            text = text.rstrip()
+
         if froms:
             text += " \nFROM "
 
index 39ef420dd6ed32747c3d4fa3da3c1c5c779f0b1c..56cca6f73d220162d94196957aa1070a1fb3470f 100644 (file)
@@ -89,6 +89,7 @@ from .elements import literal_column
 from .elements import TableValuedColumn
 from .elements import UnaryExpression
 from .operators import OperatorType
+from .sqltypes import NULLTYPE
 from .visitors import _TraverseInternalsType
 from .visitors import InternalTraversal
 from .visitors import prefix_anon_map
@@ -5167,6 +5168,8 @@ class Select(
         GenerativeSelect.__init__(self)
 
     def _scalar_type(self) -> TypeEngine[Any]:
+        if not self._raw_columns:
+            return NULLTYPE
         elem = self._raw_columns[0]
         cols = list(elem._select_iterable)
         return cols[0].type
index ad4b4db95916f19962876409b3e083899369621e..7979fd200ce9eb44478c12f1f0a4d1f7a4d3f64a 100644 (file)
@@ -9,6 +9,7 @@ from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import testing
+from sqlalchemy import true
 from sqlalchemy import tuple_
 from sqlalchemy import union
 from sqlalchemy.sql import column
@@ -77,6 +78,29 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
             "WHERE mytable.myid = myothertable.otherid",
         )
 
+    @testing.combinations(
+        (
+            lambda tbl: select().select_from(tbl).where(tbl.c.id == 123),
+            "SELECT FROM tbl WHERE tbl.id = :id_1",
+        ),
+        (lambda tbl: select().where(true()), "SELECT WHERE 1 = 1"),
+        (
+            lambda tbl: select()
+            .select_from(tbl)
+            .where(tbl.c.id == 123)
+            .exists(),
+            "EXISTS (SELECT FROM tbl WHERE tbl.id = :id_1)",
+        ),
+    )
+    def test_select_no_columns(self, stmt, expected):
+        """test #9440"""
+
+        tbl = table("tbl", column("id"))
+
+        stmt = testing.resolve_lambda(stmt, tbl=tbl)
+
+        self.assert_compile(stmt, expected)
+
     def test_new_calling_style_clauseelement_thing_that_has_iter(self):
         class Thing:
             def __clause_element__(self):