diff options
| -rw-r--r-- | doc/build/changelog/unreleased_20/9440.rst | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 3 | ||||
| -rw-r--r-- | test/sql/test_select.py | 24 |
4 files changed, 48 insertions, 0 deletions
diff --git a/doc/build/changelog/unreleased_20/9440.rst b/doc/build/changelog/unreleased_20/9440.rst new file mode 100644 index 000000000..aa2ecd783 --- /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 bc463f9a1..ad0a3b686 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 39ef420dd..56cca6f73 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 ad4b4db95..7979fd200 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): |
