From afc338e299100356b8d436af27b1548510cc3503 Mon Sep 17 00:00:00 2001 From: Violet Folino Gallo <48537601+galloviolet@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:30:08 -0700 Subject: [PATCH] fixes #12596 --- lib/sqlalchemy/sql/compiler.py | 4 +- lib/sqlalchemy/sql/elements.py | 85 ++++++++++++++-------------------- test/sql/test_compare.py | 4 +- test/sql/test_compiler.py | 8 ++-- 4 files changed, 44 insertions(+), 57 deletions(-) diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index b123acbff1..1b2e9b2f62 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2849,7 +2849,7 @@ class SQLCompiler(Compiled): elif frameclause.lower_type is elements._FrameClauseType.RANGE_CURRENT: left = "CURRENT ROW" else: - val = self.process(frameclause.lower_integer_bind, **kw) + val = self.process(frameclause.lower_bind, **kw) if ( frameclause.lower_type is elements._FrameClauseType.RANGE_PRECEDING @@ -2863,7 +2863,7 @@ class SQLCompiler(Compiled): elif frameclause.upper_type is elements._FrameClauseType.RANGE_CURRENT: right = "CURRENT ROW" else: - val = self.process(frameclause.upper_integer_bind, **kw) + val = self.process(frameclause.upper_bind, **kw) if ( frameclause.upper_type is elements._FrameClauseType.RANGE_PRECEDING diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 42dfe61106..691d28afac 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -4231,7 +4231,7 @@ class Over(ColumnElement[_T]): element: ColumnElement[_T], partition_by: Optional[_ByArgument] = None, order_by: Optional[_ByArgument] = None, - range_: Optional[typing_Tuple[Optional[int], Optional[int]]] = None, + range_: Optional[typing_Tuple[Optional[typing.Any], Optional[typing.Any]]] = None, rows: Optional[typing_Tuple[Optional[int], Optional[int]]] = None, groups: Optional[typing_Tuple[Optional[int], Optional[int]]] = None, ): @@ -4252,8 +4252,8 @@ class Over(ColumnElement[_T]): ) else: self.range_ = _FrameClause(range_) if range_ else None - self.rows = _FrameClause(rows) if rows else None - self.groups = _FrameClause(groups) if groups else None + self.rows = _FrameClause(self._interpret_int_range(rows)) if rows else None + self.groups = _FrameClause(self._interpret_int_range(groups)) if groups else None if not TYPE_CHECKING: @@ -4272,6 +4272,15 @@ class Over(ColumnElement[_T]): ] ) ) + + def _interpret_int_range(self, rows): + try: + lower = rows[0] if rows[0] is None else int(rows[0]) + upper = rows[1] if rows[1] is None else int(rows[1]) + except ValueError as ve: + raise exc.ArgumentError("Integer or None expected for rows value and groups value") from ve + + return lower, upper class _FrameClauseType(Enum): @@ -4292,70 +4301,48 @@ class _FrameClause(ClauseElement): __visit_name__ = "frame_clause" _traverse_internals: _TraverseInternalsType = [ - ("lower_integer_bind", InternalTraversal.dp_clauseelement), - ("upper_integer_bind", InternalTraversal.dp_clauseelement), + ("lower_bind", InternalTraversal.dp_clauseelement), + ("upper_bind", InternalTraversal.dp_clauseelement), ("lower_type", InternalTraversal.dp_plain_obj), ("upper_type", InternalTraversal.dp_plain_obj), ] def __init__( self, - range_: typing_Tuple[Optional[int], Optional[int]], + range_: typing_Tuple[Optional[typing.Any], Optional[typing.Any]], ): try: - r0, r1 = range_ + lower_value, upper_value = range_ except (ValueError, TypeError) as ve: raise exc.ArgumentError("2-tuple expected for range/rows") from ve - if r0 is None: + if lower_value is None: self.lower_type = _FrameClauseType.RANGE_UNBOUNDED - self.lower_integer_bind = None + self.lower_bind = None else: - try: - lower_integer = int(r0) - except ValueError as err: - raise exc.ArgumentError( - "Integer or None expected for range value" - ) from err + if lower_value == 0: + self.lower_type = _FrameClauseType.RANGE_CURRENT + self.lower_bind = None + elif lower_value < 0: + self.lower_type = _FrameClauseType.RANGE_PRECEDING + self.lower_bind = literal(abs(lower_value), type_api.NULLTYPE) else: - if lower_integer == 0: - self.lower_type = _FrameClauseType.RANGE_CURRENT - self.lower_integer_bind = None - elif lower_integer < 0: - self.lower_type = _FrameClauseType.RANGE_PRECEDING - self.lower_integer_bind = literal( - abs(lower_integer), type_api.INTEGERTYPE - ) - else: - self.lower_type = _FrameClauseType.RANGE_FOLLOWING - self.lower_integer_bind = literal( - lower_integer, type_api.INTEGERTYPE - ) + self.lower_type = _FrameClauseType.RANGE_FOLLOWING + self.lower_bind = literal(lower_value, type_api.NULLTYPE) - if r1 is None: + if upper_value is None: self.upper_type = _FrameClauseType.RANGE_UNBOUNDED - self.upper_integer_bind = None + self.upper_bind = None else: - try: - upper_integer = int(r1) - except ValueError as err: - raise exc.ArgumentError( - "Integer or None expected for range value" - ) from err + if upper_value == 0: + self.upper_type = _FrameClauseType.RANGE_CURRENT + self.upper_bind = None + elif upper_value < 0: + self.upper_type = _FrameClauseType.RANGE_PRECEDING + self.upper_bind = literal(abs(upper_value), type_api.NULLTYPE) else: - if upper_integer == 0: - self.upper_type = _FrameClauseType.RANGE_CURRENT - self.upper_integer_bind = None - elif upper_integer < 0: - self.upper_type = _FrameClauseType.RANGE_PRECEDING - self.upper_integer_bind = literal( - abs(upper_integer), type_api.INTEGERTYPE - ) - else: - self.upper_type = _FrameClauseType.RANGE_FOLLOWING - self.upper_integer_bind = literal( - upper_integer, type_api.INTEGERTYPE - ) + self.upper_type = _FrameClauseType.RANGE_FOLLOWING + self.upper_bind = literal(upper_value, type_api.NULLTYPE) class WithinGroup(ColumnElement[_T]): diff --git a/test/sql/test_compare.py b/test/sql/test_compare.py index 9c9bde1dac..e08c5aeeb4 100644 --- a/test/sql/test_compare.py +++ b/test/sql/test_compare.py @@ -1665,8 +1665,8 @@ class HasCacheKeySubclass(fixtures.TestBase): {"columns", "name", "literal_binds"}, ), "_FrameClause": ( - {"upper_integer_bind", "upper_type"} - | {"lower_type", "lower_integer_bind"}, + {"upper_bind", "upper_type"} + | {"lower_type", "lower_bind"}, {"range_"}, ), "_MemoizedSelectEntities": ( diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py index 5e86e14db7..5ab4b67a2d 100644 --- a/test/sql/test_compiler.py +++ b/test/sql/test_compiler.py @@ -3247,16 +3247,16 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL): def test_over_invalid_framespecs(self): assert_raises_message( exc.ArgumentError, - "Integer or None expected for range value", + "Integer or None expected for rows value and groups value", func.row_number().over, - range_=("foo", 8), + rows=("foo", 8), ) assert_raises_message( exc.ArgumentError, - "Integer or None expected for range value", + "Integer or None expected for rows value and groups value", func.row_number().over, - range_=(-5, "foo"), + groups=(-5, "foo"), ) assert_raises_message( -- 2.47.3