summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/unreleased_20/9440.rst11
-rw-r--r--lib/sqlalchemy/sql/compiler.py10
-rw-r--r--lib/sqlalchemy/sql/selectable.py3
-rw-r--r--test/sql/test_select.py24
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):