1
0
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:
Nick Pope
2023-12-30 07:24:30 +00:00
committed by GitHub
parent 561e16d6a7
commit 94b6f101f7
8 changed files with 256 additions and 4 deletions

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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