From: Mike Bayer Date: Tue, 7 Mar 2023 14:03:07 +0000 (-0500) Subject: resolve select to NULLTYPE if no columns X-Git-Tag: rel_2_0_6~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b97b313c6eb7f2fe4b98d011c292de4d258c508c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git resolve select to NULLTYPE if no columns 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 --- diff --git a/doc/build/changelog/unreleased_20/9440.rst b/doc/build/changelog/unreleased_20/9440.rst new file mode 100644 index 0000000000..aa2ecd783e --- /dev/null +++ b/doc/build/changelog/unreleased_20/9440.rst @@ -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. + diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index bc463f9a1e..ad0a3b6866 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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 " diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 39ef420dd6..56cca6f73d 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -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 diff --git a/test/sql/test_select.py b/test/sql/test_select.py index ad4b4db959..7979fd200c 100644 --- a/test/sql/test_select.py +++ b/test/sql/test_select.py @@ -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):