seitime-frappe/frappe/database/operator_map.py
Aarol D'Souza cf69e4bed1
fix(postgres): misc query building fixes + CI (#34831)
* fix(query): check standard field definitions

Signed-off-by: Akhil Narang <me@akhilnarang.dev>

* fix(postgres): fix order_by problem in pg

* fix(postgres): fix order_by in get_all for _test_connection_query

* fix: add check to a proper numeric fallback in _get_ifnull_fallback

* test(postgres): fix pg query used in assertion in test_permission_query

* fix(postgres): fix order_by in get_all for possible_link

* fix(postgres): fix order_by in get_all for set_modules

* fix(postgres): fix pg query count *

* fix(postgres): fix order_by in get_all for ask_pass_update

* fix(postgres): fix order_by statement in search_widget

* fix(postgres): fix order_by in get_list for get_stats

* test(postgres): normalize_sql for pg queries in test_arithmetic_operators_in_fields

* test(postgres): normalize_sql for pg queries in test_field_alias_in_group_by

* test(postgres): normalize_sql for pg queries in test_field_alias_permission_check

* test(postgres): fix order_by statement in get_all for test_db_keywords_as_fields

* test(postgres): fix order_by statement in get_all for test_prepare_select_args

* fix(treeview): use 0 instead of false to check since check field is an integer

* fix(postgres): fix order_by in get_all for sync_communication

* fix(postgres): fix order_by in get_all for get_references_across_doctypes_by_dynamic_link_field

* test(postgres): fix order_by in get_all for test_list_summary

* fix(postgres): fix order_by in get_all for email queries

* test(postgres): use order_by none and update assertion for postgres

* fix(postgres): use ILIKE to support case insensitive search in postgres

* test(test_query): update pg specific query assert to use ILIKE

* test(test_query): update test_nested_filters to use ilike instead for PG

* test(postgres): update pg query in assert to test updated qb query

* fix(search): update query to be db-agnostic

* test(postgres): normalize query for pg in test_build_match_conditions

* fix(postgres): suppress ORDER BY when SELECT DISTINCT in query for postgres specific behavior

* fix(postgres): suppress ORDER BY when GROUP BY is explicitly asked for pg specific behavior

* test(postgres): fix test behavior for pg ORDER BY drop when used with GROUP BY

* refactor: reducing noise in code by formatting code

* fix(query): use Star() to handle SQL wildcard character * correctly

* fix(postgres): display warning for ORDER BY fields that will be dropped

---------

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Akhil Narang <me@akhilnarang.dev>
2025-12-05 10:30:49 +05:30

159 lines
3.3 KiB
Python

# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import operator
from collections.abc import Callable
import frappe
from frappe.database.utils import NestedSetHierarchy
from frappe.model.db_query import get_timespan_date_range
from frappe.query_builder import Field
from frappe.query_builder.functions import Coalesce
def like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `LIKE`
Args:
key (str): field
value (str): criterion
Return:
frappe.qb: `frappe.qb` object with `LIKE`
"""
return key.like(value)
def ilike(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `ILIKE`
Args:
key (str): field
value (str): criterion
Return:
frappe.qb: `frappe.qb` object with `ILIKE`
"""
return key.ilike(value)
def func_in(key: Field, value: list | tuple) -> frappe.qb:
"""Wrapper method for `IN`.
Args:
key (str): field
value (Union[int, str]): criterion
Return:
frappe.qb: `frappe.qb` object with `IN`
"""
if isinstance(value, str):
value = value.split(",")
return key.isin(value)
def not_like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `NOT LIKE`.
Args:
key (str): field
value (str): criterion
Return:
frappe.qb: `frappe.qb` object with `NOT LIKE`
"""
return key.not_like(value)
def func_not_in(key: Field, value: list | tuple | str):
"""Wrapper method for `NOT IN`.
Args:
key (str): field
value (Union[int, str]): criterion
Return:
frappe.qb: `frappe.qb` object with `NOT IN`
"""
if isinstance(value, str):
value = value.split(",")
return key.notin(value)
def func_regex(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `REGEX`
Args:
key (str): field
value (str): criterion
Return:
frappe.qb: `frappe.qb` object with `REGEX`
"""
return key.regex(value)
def func_between(key: Field, value: list | tuple) -> frappe.qb:
"""Wrapper method for `BETWEEN`.
Args:
key (str): field
value (Union[int, str]): criterion
Return:
frappe.qb: `frappe.qb` object with `BETWEEN`
"""
return key[slice(*value)]
def func_is(key, value):
"Wrapper for IS"
match value.lower():
case "set":
return key != ""
case "not set":
return key.isnull() | (key == "")
case _:
raise ValueError("`is` operator only supports `set` and `not set` as value")
def func_timespan(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `TIMESPAN`.
Args:
key (str): field
value (str): criterion
Return:
frappe.qb: `frappe.qb` object with `TIMESPAN`
"""
return func_between(key, get_timespan_date_range(value))
# default operators
OPERATOR_MAP: dict[str, Callable] = {
"+": operator.add,
"=": operator.eq,
"-": operator.sub,
"!=": operator.ne,
"<": operator.lt,
">": operator.gt,
"<=": operator.le,
"=<": operator.le,
">=": operator.ge,
"=>": operator.ge,
"/": operator.truediv,
"*": operator.mul,
"in": func_in,
"not in": func_not_in,
"like": like,
"ilike": ilike,
"not like": not_like,
"regex": func_regex,
"between": func_between,
"is": func_is,
"timespan": func_timespan,
# TODO: Add support for custom operators (WIP) - via filters_config hooks
}
NESTED_SET_OPERATORS = frozenset(NestedSetHierarchy)