Fix duration-only expressions for SQLite and MySQL

Allow arithmetic with DurationField on SQLite/MySQL by converting
timedelta expressions to microseconds for proper backend handling.
Addresses errors with duration-only annotation queries.
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 23:04:27 +00:00
parent ec5aa2161d
commit 9be35a85d0
2 changed files with 23 additions and 0 deletions

View file

@ -1,6 +1,7 @@
import uuid import uuid
from django.conf import settings from django.conf import settings
from django.db import DatabaseError
from django.db.backends.base.operations import BaseDatabaseOperations from django.db.backends.base.operations import BaseDatabaseOperations
from django.utils import timezone from django.utils import timezone
from django.utils.duration import duration_microseconds from django.utils.duration import duration_microseconds
@ -288,6 +289,24 @@ class DatabaseOperations(BaseDatabaseOperations):
return 'FLOOR(%(lhs)s / POW(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs} return 'FLOOR(%(lhs)s / POW(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs}
return super().combine_expression(connector, sub_expressions) return super().combine_expression(connector, sub_expressions)
def combine_duration_expression(self, connector, sub_expressions):
# MySQL doesn't support direct arithmetic on INTERVAL expressions.
# For duration-only operations, we need to extract the numeric values
# and perform integer arithmetic, then format the result.
if connector not in ['+', '-']:
raise DatabaseError('Invalid connector for timedelta: %s.' % connector)
# Remove INTERVAL ... MICROSECOND wrapping if present and do numeric arithmetic
unwrapped = []
for expr in sub_expressions:
if expr.startswith('INTERVAL ') and expr.endswith(' MICROSECOND'):
# Extract the numeric part
unwrapped.append(expr[9:-12]) # Remove 'INTERVAL ' and ' MICROSECOND'
else:
unwrapped.append(expr)
# Perform the arithmetic and return as microseconds (BIGINT)
conn = ' %s ' % connector
return conn.join(unwrapped)
def get_db_converters(self, expression): def get_db_converters(self, expression):
converters = super().get_db_converters(expression) converters = super().get_db_converters(expression)
internal_type = expression.output_field.get_internal_type() internal_type = expression.output_field.get_internal_type()

View file

@ -561,6 +561,10 @@ def _sqlite_format_dtdelta(conn, lhs, rhs):
return None return None
# typecast_timestamp returns a date or a datetime without timezone. # typecast_timestamp returns a date or a datetime without timezone.
# It will be formatted as "%Y-%m-%d" or "%Y-%m-%d %H:%M:%S[.%f]" # It will be formatted as "%Y-%m-%d" or "%Y-%m-%d %H:%M:%S[.%f]"
# For timedelta, return the value as microseconds to match the expected
# format for DurationField in databases without native duration support.
if isinstance(out, datetime.timedelta):
return duration_microseconds(out)
return str(out) return str(out)