fix(query): match between behaviour for datetime fields with db_query

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
This commit is contained in:
Akhil Narang 2025-12-08 16:51:58 +05:30
parent da92e7cd36
commit 207ee7a367
No known key found for this signature in database
GPG key ID: 9DCC61E211BF645F
2 changed files with 72 additions and 0 deletions

View file

@ -79,6 +79,44 @@ def _apply_date_field_filter_conversion(value, operator: str, doctype: str, fiel
return value
def _apply_datetime_field_filter_conversion(between_values: tuple | list, doctype: str, field) -> tuple:
"""Apply date to datetime conversion for Datetime fields with 'between' operator.
Args:
between_values: Tuple/list of two values [from, to] for between filter
doctype: DocType name
field: Field name or pypika Field object
Returns:
Tuple with dates expanded to datetime ranges for Datetime fields
"""
from frappe.model.db_query import _convert_type_for_between_filters
# Extract field name
field_name = field
if "." in str(field):
field_name = field.split(".")[-1]
# Skip querying meta for core doctypes to avoid recursion
if doctype in CORE_DOCTYPES:
df = None
else:
meta = frappe.get_meta(doctype)
df = meta.get_field(field_name) if meta else None
# Standard datetime fields or Datetime fieldtype
if not (field_name in ("creation", "modified") or (df and df.fieldtype == "Datetime")):
return between_values
from_val, to_val = between_values
# Convert to datetime using db_query helper (handles strings, dates, datetimes)
from_val = _convert_type_for_between_filters(from_val, set_time=datetime.time())
to_val = _convert_type_for_between_filters(to_val, set_time=datetime.time(23, 59, 59, 999999))
return (from_val, to_val)
if TYPE_CHECKING:
from frappe.query_builder import DocType
@ -487,12 +525,22 @@ class Engine:
frappe.throw(_("Document cannot be used as a filter value"))
_operator = operator
if _operator.lower() in ("timespan", "previous", "next"):
from frappe.model.db_query import get_date_range
_value = get_date_range(_operator.lower(), _value)
_operator = "between"
# For Date fields with datetime values, convert to date to match db_query behavior
if isinstance(_value, datetime.datetime) or (
isinstance(_value, list | tuple) and any(isinstance(v, datetime.datetime) for v in _value)
):
_value = _apply_date_field_filter_conversion(_value, _operator, doctype or self.doctype, field)
# For Datetime fields with date values and 'between' operator, convert to datetime range to match db_query
if _operator.lower() == "between" and isinstance(_value, list | tuple) and len(_value) == 2:
_value = _apply_datetime_field_filter_conversion(_value, doctype or self.doctype, field)
if not _value and isinstance(_value, list | tuple | set):
_value = ("",)

View file

@ -1902,6 +1902,30 @@ class TestQuery(IntegrationTestCase):
# If we get here without PermissionError, the test passes
self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql))
def test_between_datetime_expansion(self):
"""Test that date strings are expanded to datetime ranges for Datetime fields with 'between' operator"""
# Test with creation field (standard datetime field)
query = frappe.qb.get_query(
"User",
filters={"creation": ["between", ["2025-12-01", "2025-12-01"]]},
)
sql = query.get_sql()
# Date strings should be expanded to datetime ranges
self.assertIn("2025-12-01 00:00:00", sql)
self.assertIn("2025-12-01 23:59:59", sql)
def test_timespan_datetime_expansion(self):
"""Test that timespan operator expands dates to datetime ranges for Datetime fields"""
query = frappe.qb.get_query(
"User",
filters={"creation": ["timespan", "last 7 days"]},
)
sql = query.get_sql()
# Timespan should expand dates to datetime ranges (start of first day, end of last day)
# Should have times like 00:00:00 and 23:59:59
self.assertIn("00:00:00", sql)
self.assertIn("23:59:59", sql)
# This function is used as a permission query condition hook
def test_permission_hook_condition(user):