diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py index e725dff828..24e2ee0e5f 100644 --- a/frappe/query_builder/functions.py +++ b/frappe/query_builder/functions.py @@ -1,3 +1,4 @@ +from datetime import time from enum import Enum from pypika.functions import * @@ -47,6 +48,9 @@ Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTO class _PostgresTimestamp(ArithmeticExpression): def __init__(self, datepart, timepart, alias=None): + """Postgres would need both datepart and timepart to be a string for concatenation""" + if isinstance(timepart, time) or isinstance(datepart, time): + timepart, datepart = str(timepart), str(datepart) if isinstance(datepart, str): datepart = Cast(datepart, "date") if isinstance(timepart, str): diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py index 4d3c98b6a3..c1af0d9aa2 100644 --- a/frappe/query_builder/terms.py +++ b/frappe/query_builder/terms.py @@ -1,11 +1,11 @@ -from datetime import timedelta +from datetime import time, timedelta from typing import Any from pypika.queries import QueryBuilder from pypika.terms import Criterion, Function, ValueWrapper from pypika.utils import format_alias_sql -from frappe.utils.data import format_timedelta +from frappe.utils.data import format_time, format_timedelta class NamedParameterWrapper: @@ -55,9 +55,12 @@ class ParameterizedValueWrapper(ValueWrapper): value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) sql = param_wrapper.get_sql(param_value=value_sql, **kwargs) else: - # * BUG: pypika doesen't parse timedeltas + # * BUG: pypika doesen't parse timedeltas and datetime.time if isinstance(self.value, timedelta): self.value = format_timedelta(self.value) + elif isinstance(self.value, time): + self.value = format_time(self.value) + sql = self.get_value_sql( quote_char=quote_char, secondary_quote_char=secondary_quote_char, diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index 06f2f5f6a7..b80e3ad8e2 100644 --- a/frappe/tests/test_query_builder.py +++ b/frappe/tests/test_query_builder.py @@ -1,5 +1,6 @@ import unittest from collections.abc import Callable +from datetime import time import frappe from frappe.query_builder import Case @@ -74,6 +75,26 @@ class TestCustomFunctionsMariaDB(FrappeTestCase): str(select_query).lower(), ) + def test_time(self): + note = frappe.qb.DocType("Note") + self.assertEqual( + "TIMESTAMP('2021-01-01','00:00:21')", CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql() + ) + + select_query = frappe.qb.from_(note).select( + CombineDatetime(note.posting_date, note.posting_time) + ) + self.assertIn("select timestamp(`posting_date`,`posting_time`)", str(select_query).lower()) + + select_query = select_query.where( + CombineDatetime(note.posting_date, note.posting_time) + >= CombineDatetime("2021-01-01", time(0, 0, 1)) + ) + self.assertIn( + "timestamp(`posting_date`,`posting_time`)>=timestamp('2021-01-01','00:00:01')", + str(select_query).lower(), + ) + def test_cast(self): note = frappe.qb.DocType("Note") self.assertEqual("CONCAT(name,'')", Cast_(note.name, "varchar").get_sql()) @@ -141,6 +162,28 @@ class TestCustomFunctionsPostgres(FrappeTestCase): '"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower() ) + def test_time(self): + note = frappe.qb.DocType("Note") + + self.assertEqual( + "CAST('2021-01-01' AS DATE)+CAST('00:00:21' AS TIME)", + CombineDatetime("2021-01-01", time(0, 0, 21)).get_sql(), + ) + + select_query = frappe.qb.from_(note).select( + CombineDatetime(note.posting_date, note.posting_time) + ) + self.assertIn('select "posting_date"+"posting_time"', str(select_query).lower()) + + select_query = select_query.where( + CombineDatetime(note.posting_date, note.posting_time) + >= CombineDatetime("2021-01-01", time(0, 0, 1)) + ) + self.assertIn( + """where "posting_date"+"posting_time">=cast('2021-01-01' as date)+cast('00:00:01' as time)""", + str(select_query).lower(), + ) + def test_cast(self): note = frappe.qb.DocType("Note") self.assertEqual("CAST(name AS VARCHAR)", Cast_(note.name, "varchar").get_sql())