--- /dev/null
+.. change::
+ :tags: usecase, sql
+ :tickets: 11671
+
+ Add the `exclude` parameter to the `Over` construct and all `.over()`
+ methods, enabling SQL standard frame exclusion clauses
+ `EXCLUDE CURRENT ROW`, `EXCLUDE GROUP`, `EXCLUDE TIES`,
+ `EXCLUDE NO OTHERS` in window functions.
+ Pull request courtesy of Varun Chawla.
\ No newline at end of file
range_: _FrameIntTuple | FrameClause | None = None,
rows: _FrameIntTuple | FrameClause | None = None,
groups: _FrameIntTuple | FrameClause | None = None,
+ exclude: str | None = None,
) -> Over[_T]:
r"""Produce an :class:`.Over` object against a function.
.. versionadded:: 2.0.40
+ :param exclude: optional string for the frame exclusion clause.
+ This is a string value which can be one of ``CURRENT ROW``,
+ ``GROUP``, ``TIES``, or ``NO OTHERS`` and will render an
+ EXCLUDE clause within the window frame specification. Requires
+ that one of ``rows``, ``range_``, or ``groups`` is also specified.
+
+ .. versionadded:: 2.1
+
This function is also available from the :data:`~.expression.func`
construct itself via the :meth:`.FunctionElement.over` method.
:func:`_expression.within_group`
""" # noqa: E501
- return Over(element, partition_by, order_by, range_, rows, groups)
+ return Over(
+ element,
+ partition_by,
+ order_by,
+ range_,
+ rows,
+ groups,
+ exclude,
+ )
@_document_text_coercion("text", ":func:`.text`", ":paramref:`.text.text`")
r"^(?:RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT)$", re.I
)
FK_INITIALLY = re.compile(r"^(?:DEFERRED|IMMEDIATE)$", re.I)
+_WINDOW_EXCLUDE_RE = re.compile(
+ r"^(?:CURRENT ROW|GROUP|TIES|NO OTHERS)$", re.I
+)
BIND_PARAMS = re.compile(r"(?<![:\w\$\x5c]):([\w\$]+)(?![:\w\$])", re.UNICODE)
BIND_PARAMS_ESC = re.compile(r"\x5c(:[\w\$]*)(?![:\w\$])", re.UNICODE)
else:
range_ = None
+ if range_ is not None and over.exclude is not None:
+ range_ += " EXCLUDE " + self.preparer.validate_sql_phrase(
+ over.exclude, _WINDOW_EXCLUDE_RE
+ )
+
return "%s OVER (%s)" % (
text,
" ".join(
("range_", InternalTraversal.dp_clauseelement),
("rows", InternalTraversal.dp_clauseelement),
("groups", InternalTraversal.dp_clauseelement),
+ ("exclude", InternalTraversal.dp_string),
]
order_by: Optional[ClauseList] = None
range_: FrameClause | None
rows: FrameClause | None
groups: FrameClause | None
+ exclude: str | None
def __init__(
self,
range_: _FrameIntTuple | FrameClause | None = None,
rows: _FrameIntTuple | FrameClause | None = None,
groups: _FrameIntTuple | FrameClause | None = None,
+ exclude: str | None = None,
):
self.element = element
if order_by is not None:
self.rows = FrameClause._parse(rows, coerce_int=True)
self.groups = FrameClause._parse(groups, coerce_int=True)
+ self.exclude = exclude
+
+ if exclude is not None and (
+ range_ is None and rows is None and groups is None
+ ):
+ raise exc.ArgumentError(
+ "'exclude' requires that one of 'rows', "
+ "'range_', or 'groups' is also specified"
+ )
+
if not TYPE_CHECKING:
@util.memoized_property
rows: _FrameIntTuple | FrameClause | None = None,
range_: _FrameIntTuple | FrameClause | None = None,
groups: _FrameIntTuple | FrameClause | None = None,
+ exclude: str | None = None,
) -> Over[_T]:
"""Produce an OVER clause against this :class:`.WithinGroup`
construct.
range_=range_,
rows=rows,
groups=groups,
+ exclude=exclude,
)
@overload
range_: _FrameIntTuple | FrameClause | None = None,
rows: _FrameIntTuple | FrameClause | None = None,
groups: _FrameIntTuple | FrameClause | None = None,
+ exclude: str | None = None,
) -> Over[_T]:
"""Produce an OVER clause against this filtered function.
range_=range_,
rows=rows,
groups=groups,
+ exclude=exclude,
)
def within_group(
rows: _FrameIntTuple | FrameClause | None = None,
range_: _FrameIntTuple | FrameClause | None = None,
groups: _FrameIntTuple | FrameClause | None = None,
+ exclude: str | None = None,
) -> Over[_T]:
"""Produce an OVER clause against this function.
rows=rows,
range_=range_,
groups=groups,
+ exclude=exclude,
)
def aggregate_order_by(
checkparams={"param_1": 1, "param_2": 3},
)
+ @testing.combinations(
+ (
+ "rows_current_row",
+ {"rows": (None, 0)},
+ "CURRENT ROW",
+ "SELECT row_number() OVER "
+ "(ORDER BY mytable.myid ROWS BETWEEN UNBOUNDED "
+ "PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW)"
+ " AS anon_1 FROM mytable",
+ {},
+ ),
+ (
+ "range_group",
+ {"range_": (None, 0)},
+ "GROUP",
+ "SELECT row_number() OVER "
+ "(ORDER BY mytable.myid RANGE BETWEEN UNBOUNDED "
+ "PRECEDING AND CURRENT ROW EXCLUDE GROUP)"
+ " AS anon_1 FROM mytable",
+ {},
+ ),
+ (
+ "groups_ties",
+ {"groups": (-1, 1)},
+ "TIES",
+ "SELECT row_number() OVER "
+ "(ORDER BY mytable.myid GROUPS BETWEEN "
+ ":param_1 PRECEDING AND :param_2 FOLLOWING EXCLUDE TIES)"
+ " AS anon_1 FROM mytable",
+ {"param_1": 1, "param_2": 1},
+ ),
+ (
+ "rows_no_others",
+ {"rows": (None, None)},
+ "NO OTHERS",
+ "SELECT row_number() OVER "
+ "(ORDER BY mytable.myid ROWS BETWEEN UNBOUNDED "
+ "PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE NO OTHERS)"
+ " AS anon_1 FROM mytable",
+ {},
+ ),
+ (
+ "rows_lowercase_ties",
+ {"rows": (None, 0)},
+ "ties",
+ "SELECT row_number() OVER "
+ "(ORDER BY mytable.myid ROWS BETWEEN UNBOUNDED "
+ "PRECEDING AND CURRENT ROW EXCLUDE ties)"
+ " AS anon_1 FROM mytable",
+ {},
+ ),
+ id_="iaaaa",
+ )
+ def test_over_frame_exclude(
+ self, frame_kwargs, exclude, expected_sql, checkparams
+ ):
+ self.assert_compile(
+ select(
+ func.row_number().over(
+ order_by=table1.c.myid, exclude=exclude, **frame_kwargs
+ )
+ ),
+ expected_sql,
+ checkparams=checkparams,
+ )
+
+ def test_over_frame_exclude_invalid(self):
+ # invalid exclude value raises at compile time
+ assert_raises_message(
+ exc.CompileError,
+ "Unexpected SQL phrase: 'INVALID'",
+ select(
+ func.row_number().over(
+ order_by=table1.c.myid,
+ rows=(None, 0),
+ exclude="INVALID",
+ )
+ ).compile,
+ )
+
+ def test_over_frame_exclude_requires_frame_spec(self):
+ # exclude without rows/range_/groups raises at construction time
+ with expect_raises_message(
+ exc.ArgumentError,
+ "'exclude' requires that one of 'rows', "
+ "'range_', or 'groups' is also specified",
+ ):
+ func.row_number().over(
+ order_by=table1.c.myid,
+ exclude="CURRENT ROW",
+ )
+
def test_over_invalid_framespecs(self):
with expect_raises_message(
exc.ArgumentError,