Skip to content

Commit

Permalink
Fixed #29850 -- Added exclusion support to RowRange and ValueRange.
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahboyce committed Oct 28, 2023
1 parent 618f946 commit f278478
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 7 deletions.
1 change: 1 addition & 0 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class BaseDatabaseFeatures:
supports_over_clause = False
supports_frame_range_fixed_distance = False
only_supports_unbounded_with_preceding_and_following = False
supports_frame_exclusion = False

# Does the backend support CAST with precision?
supports_cast_with_precision = True
Expand Down
4 changes: 4 additions & 0 deletions django/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,7 @@ def supports_collation_on_charfield(self):
@cached_property
def supports_primitives_in_json_field(self):
return self.connection.oracle_version >= (21,)

@cached_property
def supports_frame_exclusion(self):
return self.connection.oracle_version >= (21,)
1 change: 1 addition & 0 deletions django/db/backends/postgresql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_casted_case_in_updates = True
supports_over_clause = True
only_supports_unbounded_with_preceding_and_following = True
supports_frame_exclusion = True
supports_aggregate_filter_clause = True
supported_explain_formats = {"JSON", "TEXT", "XML", "YAML"}
supports_deferrable_unique_constraints = True
Expand Down
1 change: 1 addition & 0 deletions django/db/backends/sqlite3/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_defer_constraint_checks = True
supports_over_clause = True
supports_frame_range_fixed_distance = Database.sqlite_version_info >= (3, 28, 0)
supports_frame_exclusion = Database.sqlite_version_info >= (3, 28, 0)
supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1)
supports_order_by_nulls_modifier = Database.sqlite_version_info >= (3, 30, 0)
# NULLS LAST/FIRST emulation on < 3.30 requires subquery wrapping.
Expand Down
2 changes: 2 additions & 0 deletions django/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.db.models.enums import __all__ as enums_all
from django.db.models.expressions import (
Case,
Exclusion,
Exists,
Expression,
ExpressionList,
Expand Down Expand Up @@ -76,6 +77,7 @@
"ProtectedError",
"RestrictedError",
"Case",
"Exclusion",
"Exists",
"Expression",
"ExpressionList",
Expand Down
32 changes: 29 additions & 3 deletions django/db/models/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import inspect
from collections import defaultdict
from decimal import Decimal
from enum import Enum
from types import NoneType
from uuid import UUID

Expand Down Expand Up @@ -1848,6 +1849,13 @@ def get_group_by_cols(self):
return group_by_cols


class Exclusion(Enum):
CURRENT_ROW = "CURRENT ROW"
GROUP = "GROUP"
TIES = "TIES"
NO_OTHERS = "NO OTHERS"


class WindowFrame(Expression):
"""
Model the frame clause in window expressions. There are two types of frame
Expand All @@ -1859,23 +1867,37 @@ class WindowFrame(Expression):

template = "%(frame_type)s BETWEEN %(start)s AND %(end)s"

def __init__(self, start=None, end=None):
def __init__(self, start=None, end=None, exclusion=None):
self.start = Value(start)
self.end = Value(end)
self.exclusion = exclusion

def set_source_expressions(self, exprs):
self.start, self.end = exprs

def get_source_expressions(self):
return [self.start, self.end]

def get_exclusion(self):
if not connection.features.supports_frame_exclusion:
raise NotSupportedError(
"This backend does not support window frame exclusions."
)
if self.exclusion in Exclusion.__members__.values():
return f"EXCLUDE {self.exclusion.value}"
raise ValueError("exclusion must be a valid Exclusion value.")

def as_sql(self, compiler, connection):
connection.ops.check_expression_support(self)
start, end = self.window_frame_start_end(
connection, self.start.value, self.end.value
)
window_frame_template_parts = [self.template]
if self.exclusion is not None:
exclusion_as_sql = self.get_exclusion()
window_frame_template_parts.append(exclusion_as_sql)
return (
self.template
" ".join(window_frame_template_parts)
% {
"frame_type": self.frame_type,
"start": start,
Expand Down Expand Up @@ -1908,7 +1930,11 @@ def __str__(self):
end = "%d %s" % (abs(self.end.value), connection.ops.PRECEDING)
else:
end = connection.ops.UNBOUNDED_FOLLOWING
return self.template % {

window_frame_template_parts = [self.template]
if self.exclusion in Exclusion.__members__.values():
window_frame_template_parts.append(f"EXCLUDE {self.exclusion.value}")
return " ".join(window_frame_template_parts) % {
"frame_type": self.frame_type,
"start": start,
"end": end,
Expand Down
28 changes: 25 additions & 3 deletions docs/ref/models/expressions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ Frames
For a window frame, you can choose either a range-based sequence of rows or an
ordinary sequence of rows.

.. class:: ValueRange(start=None, end=None)
.. class:: ValueRange(start=None, end=None, exclusion=None)

.. attribute:: frame_type

Expand All @@ -899,7 +899,7 @@ ordinary sequence of rows.
the standard start and end points, such as ``CURRENT ROW`` and ``UNBOUNDED
FOLLOWING``.

.. class:: RowRange(start=None, end=None)
.. class:: RowRange(start=None, end=None, exclusion=None)

.. attribute:: frame_type

Expand All @@ -911,6 +911,27 @@ Both classes return SQL with the template:

%(frame_type)s BETWEEN %(start)s AND %(end)s


.. class:: Exclusion

.. versionadded:: 5.1

.. attribute:: CURRENT_ROW

.. attribute:: GROUP

.. attribute:: TIES

.. attribute:: NO_OTHERS

If ``exclusion`` is an attribute of
:class:`~django.db.models.expressions.Exclusion`, this returns for supported
databases:

.. code-block:: sql

%(frame_type)s BETWEEN %(start)s AND %(end)s EXCLUDE (exclusion)s

Frames narrow the rows that are used for computing the result. They shift from
some start point to some specified end point. Frames can be used with and
without partitions, but it's often a good idea to specify an ordering of the
Expand Down Expand Up @@ -976,7 +997,8 @@ released between twelve months before and twelve months after the each movie:
.. versionchanged:: 5.1

Support for positive integer ``start`` and negative integer ``end`` was
added for ``RowRange``.
added for ``RowRange``. ``exclusion`` was added to ``RowRange`` and
``ValueRange``.

.. currentmodule:: django.db.models

Expand Down
5 changes: 5 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ Models
* :class:`~django.db.models.expressions.RowRange` now accept positive integers
for the ``start`` argument and negative integers for the ``end`` argument.

* The new ``exclusion`` argument of
:class:`~django.db.models.expressions.RowRange` and
:class:`~django.db.models.expressions.ValueRange` adds an exclusion clause to
the generated SQL when set to a for supported databases.

Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading

0 comments on commit f278478

Please sign in to comment.