2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
数据库方言兼容性辅助函数
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2025-12-13 22:27:00 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from sqlalchemy import func
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 19:32:57 +08:00
|
|
|
|
def escape_like_pattern(pattern: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
转义 SQL LIKE 语句中的特殊字符(%、_、\\)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
pattern: 原始搜索模式
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
转义后的模式,可安全用于 LIKE 查询(需配合 escape="\\\\")
|
|
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
>>> escape_like_pattern("hello_world%test")
|
|
|
|
|
|
'hello\\\\_world\\\\%test'
|
|
|
|
|
|
"""
|
|
|
|
|
|
return pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def safe_truncate_escaped(escaped: str, max_len: int) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
安全截断已转义的字符串,避免截断在转义序列中间
|
|
|
|
|
|
|
|
|
|
|
|
转义后的字符串中,反斜杠总是成对出现(\\\\)或作为转义符(\\%, \\_)。
|
|
|
|
|
|
如果在某个位置截断导致末尾有奇数个反斜杠,说明截断发生在转义序列中间,
|
|
|
|
|
|
需要去掉最后一个反斜杠以保持转义完整性。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
escaped: 已经过 escape_like_pattern 处理的字符串
|
|
|
|
|
|
max_len: 最大长度
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
截断后的字符串,保证不会破坏转义序列
|
|
|
|
|
|
"""
|
|
|
|
|
|
if len(escaped) <= max_len:
|
|
|
|
|
|
return escaped
|
|
|
|
|
|
|
|
|
|
|
|
truncated = escaped[:max_len]
|
|
|
|
|
|
|
|
|
|
|
|
# 统计末尾连续的反斜杠数量
|
|
|
|
|
|
trailing_backslashes = 0
|
|
|
|
|
|
for i in range(len(truncated) - 1, -1, -1):
|
|
|
|
|
|
if truncated[i] == "\\":
|
|
|
|
|
|
trailing_backslashes += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 如果末尾反斜杠数量为奇数,说明截断在转义序列中间
|
|
|
|
|
|
# 需要去掉最后一个反斜杠
|
|
|
|
|
|
if trailing_backslashes % 2 == 1:
|
|
|
|
|
|
truncated = truncated[:-1]
|
|
|
|
|
|
|
|
|
|
|
|
return truncated
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-13 22:27:00 +08:00
|
|
|
|
def date_trunc_portable(dialect_name: str, interval: str, column: Any) -> Any:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
跨数据库的日期截断函数
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
dialect_name: 数据库方言名称 ('postgresql', 'sqlite', 'mysql')
|
|
|
|
|
|
interval: 时间间隔 ('day', 'week', 'month', 'year')
|
|
|
|
|
|
column: 日期列
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
SQLAlchemy ClauseElement
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
NotImplementedError: 不支持的数据库方言
|
|
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
>>> # PostgreSQL
|
|
|
|
|
|
>>> period_func = date_trunc_portable('postgresql', 'week', Usage.created_at)
|
|
|
|
|
|
>>> # 等价于: func.date_trunc('week', Usage.created_at)
|
|
|
|
|
|
|
|
|
|
|
|
>>> # SQLite
|
|
|
|
|
|
>>> period_func = date_trunc_portable('sqlite', 'month', Usage.created_at)
|
|
|
|
|
|
>>> # 等价于: func.strftime("%Y-%m", Usage.created_at)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if dialect_name == "postgresql":
|
|
|
|
|
|
# PostgreSQL 使用 date_trunc 函数
|
|
|
|
|
|
return func.date_trunc(interval, column)
|
|
|
|
|
|
|
|
|
|
|
|
elif dialect_name == "sqlite":
|
|
|
|
|
|
# SQLite 使用 strftime 函数
|
|
|
|
|
|
format_map = {
|
|
|
|
|
|
"year": "%Y",
|
|
|
|
|
|
"month": "%Y-%m",
|
|
|
|
|
|
"week": "%Y-%W",
|
|
|
|
|
|
"day": "%Y-%m-%d",
|
|
|
|
|
|
}
|
|
|
|
|
|
if interval not in format_map:
|
|
|
|
|
|
raise ValueError(f"Unsupported interval for SQLite: {interval}")
|
|
|
|
|
|
return func.strftime(format_map[interval], column)
|
|
|
|
|
|
|
|
|
|
|
|
elif dialect_name == "mysql":
|
|
|
|
|
|
# MySQL 使用 date_format 函数
|
|
|
|
|
|
format_map = {
|
|
|
|
|
|
"year": "%Y",
|
|
|
|
|
|
"month": "%Y-%m",
|
|
|
|
|
|
"day": "%Y-%m-%d",
|
|
|
|
|
|
}
|
|
|
|
|
|
if interval not in format_map:
|
|
|
|
|
|
raise ValueError(f"Unsupported interval for MySQL: {interval}")
|
|
|
|
|
|
return func.date_format(column, format_map[interval])
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise NotImplementedError(
|
|
|
|
|
|
f"Unsupported database dialect: {dialect_name}. "
|
|
|
|
|
|
f"Supported dialects: postgresql, sqlite, mysql"
|
|
|
|
|
|
)
|