mirror of
https://github.com/django/django.git
synced 2025-11-07 07:15:35 +00:00
Fixed #29049 -- Added slicing notation to F expressions.
Co-authored-by: Priyansh Saxena <askpriyansh@gmail.com> Co-authored-by: Niclas Olofsson <n@niclasolofsson.se> Co-authored-by: David Smith <smithdc@gmail.com> Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com> Co-authored-by: Abhinav Yadav <abhinav.sny.2002@gmail.com>
This commit is contained in:
@@ -234,6 +234,12 @@ class ArrayField(CheckFieldDefaultMixin, Field):
|
||||
}
|
||||
)
|
||||
|
||||
def slice_expression(self, expression, start, length):
|
||||
# If length is not provided, don't specify an end to slice to the end
|
||||
# of the array.
|
||||
end = None if length is None else start + length - 1
|
||||
return SliceTransform(start, end, expression)
|
||||
|
||||
|
||||
class ArrayRHSMixin:
|
||||
def __init__(self, lhs, rhs):
|
||||
@@ -351,9 +357,11 @@ class SliceTransform(Transform):
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
lhs, params = compiler.compile(self.lhs)
|
||||
if not lhs.endswith("]"):
|
||||
lhs = "(%s)" % lhs
|
||||
return "%s[%%s:%%s]" % lhs, (*params, self.start, self.end)
|
||||
# self.start is set to 1 if slice start is not provided.
|
||||
if self.end is None:
|
||||
return f"({lhs})[%s:]", (*params, self.start)
|
||||
else:
|
||||
return f"({lhs})[%s:%s]", (*params, self.start, self.end)
|
||||
|
||||
|
||||
class SliceTransformFactory:
|
||||
|
||||
@@ -851,6 +851,9 @@ class F(Combinable):
|
||||
def __repr__(self):
|
||||
return "{}({})".format(self.__class__.__name__, self.name)
|
||||
|
||||
def __getitem__(self, subscript):
|
||||
return Sliced(self, subscript)
|
||||
|
||||
def resolve_expression(
|
||||
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
|
||||
):
|
||||
@@ -925,6 +928,63 @@ class OuterRef(F):
|
||||
return self
|
||||
|
||||
|
||||
class Sliced(F):
|
||||
"""
|
||||
An object that contains a slice of an F expression.
|
||||
|
||||
Object resolves the column on which the slicing is applied, and then
|
||||
applies the slicing if possible.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, subscript):
|
||||
super().__init__(obj.name)
|
||||
self.obj = obj
|
||||
if isinstance(subscript, int):
|
||||
if subscript < 0:
|
||||
raise ValueError("Negative indexing is not supported.")
|
||||
self.start = subscript + 1
|
||||
self.length = 1
|
||||
elif isinstance(subscript, slice):
|
||||
if (subscript.start is not None and subscript.start < 0) or (
|
||||
subscript.stop is not None and subscript.stop < 0
|
||||
):
|
||||
raise ValueError("Negative indexing is not supported.")
|
||||
if subscript.step is not None:
|
||||
raise ValueError("Step argument is not supported.")
|
||||
if subscript.stop and subscript.start and subscript.stop < subscript.start:
|
||||
raise ValueError("Slice stop must be greater than slice start.")
|
||||
self.start = 1 if subscript.start is None else subscript.start + 1
|
||||
if subscript.stop is None:
|
||||
self.length = None
|
||||
else:
|
||||
self.length = subscript.stop - (subscript.start or 0)
|
||||
else:
|
||||
raise TypeError("Argument to slice must be either int or slice instance.")
|
||||
|
||||
def __repr__(self):
|
||||
start = self.start - 1
|
||||
stop = None if self.length is None else start + self.length
|
||||
subscript = slice(start, stop)
|
||||
return f"{self.__class__.__qualname__}({self.obj!r}, {subscript!r})"
|
||||
|
||||
def resolve_expression(
|
||||
self,
|
||||
query=None,
|
||||
allow_joins=True,
|
||||
reuse=None,
|
||||
summarize=False,
|
||||
for_save=False,
|
||||
):
|
||||
resolved = query.resolve_ref(self.name, allow_joins, reuse, summarize)
|
||||
if isinstance(self.obj, (OuterRef, self.__class__)):
|
||||
expr = self.obj.resolve_expression(
|
||||
query, allow_joins, reuse, summarize, for_save
|
||||
)
|
||||
else:
|
||||
expr = resolved
|
||||
return resolved.output_field.slice_expression(expr, self.start, self.length)
|
||||
|
||||
|
||||
@deconstructible(path="django.db.models.Func")
|
||||
class Func(SQLiteNumericMixin, Expression):
|
||||
"""An SQL function call."""
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.core import checks, exceptions, validators
|
||||
from django.db import connection, connections, router
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
||||
from django.db.utils import NotSupportedError
|
||||
from django.utils import timezone
|
||||
from django.utils.choices import (
|
||||
BlankChoiceIterator,
|
||||
@@ -1143,6 +1144,10 @@ class Field(RegisterLookupMixin):
|
||||
"""Return the value of this field in the given model instance."""
|
||||
return getattr(obj, self.attname)
|
||||
|
||||
def slice_expression(self, expression, start, length):
|
||||
"""Return a slice of this field."""
|
||||
raise NotSupportedError("This field does not support slicing.")
|
||||
|
||||
|
||||
class BooleanField(Field):
|
||||
empty_strings_allowed = False
|
||||
@@ -1303,6 +1308,11 @@ class CharField(Field):
|
||||
kwargs["db_collation"] = self.db_collation
|
||||
return name, path, args, kwargs
|
||||
|
||||
def slice_expression(self, expression, start, length):
|
||||
from django.db.models.functions import Substr
|
||||
|
||||
return Substr(expression, start, length)
|
||||
|
||||
|
||||
class CommaSeparatedIntegerField(CharField):
|
||||
default_validators = [validators.validate_comma_separated_integer_list]
|
||||
@@ -2497,6 +2507,11 @@ class TextField(Field):
|
||||
kwargs["db_collation"] = self.db_collation
|
||||
return name, path, args, kwargs
|
||||
|
||||
def slice_expression(self, expression, start, length):
|
||||
from django.db.models.functions import Substr
|
||||
|
||||
return Substr(expression, start, length)
|
||||
|
||||
|
||||
class TimeField(DateTimeCheckMixin, Field):
|
||||
empty_strings_allowed = False
|
||||
|
||||
Reference in New Issue
Block a user