refactor(sql): complete modularization of search builders and ID resolution
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
Modular search builder and query generators for Aether.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, List, Optional
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import text
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -123,3 +125,81 @@ def sql_where_qry_part(qry_dict_li: list) -> tuple[str, dict]|bool:
|
||||
data[field] = val
|
||||
return ' '.join(clauses), data
|
||||
return False
|
||||
|
||||
def sql_search_qry_part(
|
||||
search_query: Any,
|
||||
searchable_fields: List[str]|None = None,
|
||||
max_depth: int = 5,
|
||||
table_name: str|None = None,
|
||||
) -> tuple[str, dict]:
|
||||
"""Recursively builds a SQL WHERE clause from a SearchQuery model."""
|
||||
from app.db_sql import db
|
||||
data = {}
|
||||
param_counter = [0]
|
||||
|
||||
def get_param_name():
|
||||
param_counter[0] += 1
|
||||
return f"sp_{param_counter[0]}"
|
||||
|
||||
operator_map = {
|
||||
"eq": "=", "ne": "!=", "gt": ">", "gte": ">=", "lt": "<", "lte": "<=",
|
||||
"like": "LIKE", "in": "IN", "is_null": "IS NULL", "is_not_null": "IS NOT NULL",
|
||||
"contains": "LIKE", "icontains": "LIKE", "startswith": "LIKE", "istartswith": "LIKE",
|
||||
"endswith": "LIKE", "iendswith": "LIKE"
|
||||
}
|
||||
|
||||
def process_node(query_node, current_depth: int) -> str:
|
||||
if current_depth > max_depth:
|
||||
raise HTTPException(status_code=400, detail=f"Search query too complex.")
|
||||
clauses = []
|
||||
if hasattr(query_node, 'query_string') and query_node.query_string:
|
||||
if query_node.query_string == '%': pass
|
||||
else:
|
||||
use_match = True
|
||||
if table_name:
|
||||
try: db.execute(text(f"SELECT default_qry_str FROM `{table_name}` LIMIT 0"))
|
||||
except: use_match = False
|
||||
else: use_match = False
|
||||
if use_match:
|
||||
p_name = get_param_name()
|
||||
clauses.append(f"MATCH( default_qry_str ) AGAINST( :{p_name} IN BOOLEAN MODE )")
|
||||
data[p_name] = query_node.query_string
|
||||
elif searchable_fields:
|
||||
like_clauses = []
|
||||
for field in searchable_fields:
|
||||
if not any(x in field for x in ['_id', 'enable', 'hide', 'priority', 'sort', 'group', 'created_on', 'updated_on']):
|
||||
f_p_name = get_param_name()
|
||||
like_clauses.append(f"`{field}` LIKE :{f_p_name}")
|
||||
data[f_p_name] = f"%{query_node.query_string}%"
|
||||
if like_clauses: clauses.append(f"({' OR '.join(like_clauses)})")
|
||||
for filter_attr in ['and_filters', 'or_filters']:
|
||||
if hasattr(query_node, filter_attr) and getattr(query_node, filter_attr):
|
||||
node_clauses = []
|
||||
for item in getattr(query_node, filter_attr):
|
||||
if hasattr(item, 'field'):
|
||||
clause, item_data = process_filter(item)
|
||||
node_clauses.append(clause); data.update(item_data)
|
||||
else: node_clauses.append(f"({process_node(item, current_depth + 1)})")
|
||||
if node_clauses:
|
||||
joiner = ' AND ' if 'and' in filter_attr else ' OR '
|
||||
clauses.append(f"({joiner.join(node_clauses)})")
|
||||
return ' AND '.join(clauses)
|
||||
|
||||
def process_filter(f) -> tuple[str, dict]:
|
||||
if searchable_fields is not None and f.field not in searchable_fields:
|
||||
raise HTTPException(status_code=400, detail=f"Unauthorized search field '{f.field}'")
|
||||
sql_op = operator_map.get(f.op.lower())
|
||||
if not sql_op: raise HTTPException(status_code=400, detail=f"Unsupported operator: {f.op}")
|
||||
filter_data = {}
|
||||
if f.op.lower() in ['is_null', 'is_not_null']: clause = f"`{f.field}` {sql_op}"
|
||||
else:
|
||||
p_name = get_param_name()
|
||||
if f.op.lower() == 'in': clause = f"`{f.field}` IN (:{p_name})"; filter_data[p_name] = f.value
|
||||
elif f.op.lower() in ['contains', 'icontains']: clause = f"`{f.field}` LIKE :{p_name}"; filter_data[p_name] = f"%{f.value}%"
|
||||
elif f.op.lower() in ['startswith', 'istartswith']: clause = f"`{f.field}` LIKE :{p_name}"; filter_data[p_name] = f"{f.value}%"
|
||||
elif f.op.lower() in ['endswith', 'iendswith']: clause = f"`{f.field}` LIKE :{p_name}"; filter_data[p_name] = f"%{f.value}"
|
||||
else: clause = f"`{f.field}` {sql_op} :{p_name}"; filter_data[p_name] = f.value
|
||||
return clause, filter_data
|
||||
|
||||
sql_where = process_node(search_query, 1)
|
||||
return (f"AND ({sql_where})", data) if sql_where else ("", {})
|
||||
|
||||
Reference in New Issue
Block a user