Files
playwrite/db/mysql_connection.py
Misaka Server f02858d5dc feat: add MySQL database support with SQL translation
- Add MySQLConnection class with automatic SQL Server to MySQL translation
- Add connection factory to support both SQL Server and MySQL
- Update config schema to support MySQL configuration (host, port, db_type)
- Update default config to use MySQL (localhost:3306)
- Translate table names: [schema].[table] -> schema_table
- Translate placeholders: ? -> %s
- Translate MERGE statements to INSERT ... ON DUPLICATE KEY UPDATE

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 20:12:32 +08:00

325 lines
9.7 KiB
Python

"""
MySQL 数据库连接组件
提供 MySQL 数据库连接和查询接口,实现与 DatabaseConnection 相同的接口。
包含自动 SQL 转换功能,将 SQL Server SQL 转换为 MySQL 兼容格式。
"""
import mysql.connector
from typing import List, Dict, Any, Optional
import sys
import os
import re
# 添加项目根目录到 sys.path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from config.defaults import DEFAULT_APP_CONFIG
class MySQLConnection:
"""MySQL 数据库连接类,实现与 DatabaseConnection 相同的接口"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化 MySQL 数据库连接
Args:
config: 数据库配置字典,默认从 DEFAULT_APP_CONFIG 读取
"""
if config is None:
# 从默认配置获取 MySQL 配置
config = {
"host": DEFAULT_APP_CONFIG.database.host,
"port": DEFAULT_APP_CONFIG.database.port,
"database": DEFAULT_APP_CONFIG.database.database,
"user": DEFAULT_APP_CONFIG.database.username,
"password": DEFAULT_APP_CONFIG.database.password,
}
self.config = config
self.connection = None
def connect(self) -> mysql.connector.MySQLConnection:
"""
建立 MySQL 数据库连接
Returns:
mysql.connector.MySQLConnection: 数据库连接对象
"""
if self.connection is not None:
return self.connection
try:
self.connection = mysql.connector.connect(
host=self.config['host'],
port=self.config['port'],
database=self.config['database'],
user=self.config['user'],
password=self.config['password'],
charset='utf8mb4',
autocommit=False
)
print(
f"成功连接到 MySQL 数据库: {self.config['host']}:{self.config['port']}/{self.config['database']}"
)
return self.connection
except mysql.connector.Error as e:
print(f"MySQL 数据库连接失败: {e}")
raise
def disconnect(self):
"""关闭数据库连接"""
if self.connection:
self.connection.close()
self.connection = None
print("MySQL 数据库连接已关闭")
def _translate_table_name(self, sql: str) -> str:
"""
将 SQL Server 表名格式转换为 MySQL 格式
转换规则:
- [schema].[table] -> schema_table
- 表名中的空格替换为下划线
Args:
sql: SQL 语句
Returns:
转换后的 SQL 语句
"""
# 匹配 [schema].[table] 格式
pattern = r'\[([a-zA-Z_][a-zA-Z0-9_]*)\]\.\[([^\]]+)\]'
def replace_table_name(match):
schema = match.group(1)
table = match.group(2)
# 将表名中的空格替换为下划线
table = table.replace(' ', '_')
return f"{schema}_{table}"
result = re.sub(pattern, replace_table_name, sql)
return result
def _translate_placeholder(self, sql: str) -> str:
"""
将 SQL Server 占位符转换为 MySQL 格式
转换规则:
- ? -> %s
Args:
sql: SQL 语句
Returns:
转换后的 SQL 语句
"""
return sql.replace('?', '%s')
def _translate_merge(self, sql: str) -> str:
"""
将 T-SQL MERGE 语句转换为 MySQL INSERT ... ON DUPLICATE KEY UPDATE
示例输入:
MERGE [dbo].[MaterialsToBeDeleted] AS target
USING (SELECT ? AS MaterialCode, ? AS ManagerName) AS source
ON (target.MaterialCode = source.MaterialCode)
WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName
WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (...);
示例输出:
INSERT INTO dbo_MaterialsToBeDeleted (MaterialCode, ManagerName)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName);
Args:
sql: SQL 语句
Returns:
转换后的 SQL 语句
"""
# 检测是否为 MERGE 语句
if not re.match(r'\s*MERGE', sql, re.IGNORECASE):
return sql
# 解析 MERGE 语句的各个部分
# 这是一个简化的实现,假设 MERGE 语句遵循标准格式
# 先转换表名,再解析
sql_with_translated_table = self._translate_table_name(sql)
# 提取目标表
target_match = re.search(r'MERGE\s+(\S+)\s+AS\s+target', sql_with_translated_table, re.IGNORECASE)
if not target_match:
return sql
target_table = target_match.group(1)
# 提取 INSERT 的列
insert_match = re.search(r'INSERT\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)', sql, re.IGNORECASE)
if not insert_match:
return sql
columns = insert_match.group(1).strip()
values_part = insert_match.group(2).strip()
# 提取 UPDATE 部分
update_match = re.search(r'UPDATE\s+SET\s+([^\s]+)\s*=\s*source\.([^\s]+)', sql, re.IGNORECASE)
if not update_match:
return sql
update_column = update_match.group(1)
# 从 USING 子句中统计占位符数量
# 匹配: USING (SELECT ? AS MaterialCode, ? AS ManagerName) AS source
using_match = re.search(r'USING\s*\((.+)\)\s+AS\s+source', sql, re.IGNORECASE | re.DOTALL)
if not using_match:
return sql
using_clause = using_match.group(1)
# 从 SELECT 部分提取
select_match = re.search(r'SELECT\s+(.+)', using_clause, re.IGNORECASE)
if not select_match:
return sql
using_select = select_match.group(1)
# 计算占位符(?)的数量
placeholder_count = using_select.count('?')
# 生成相应数量的 %s 占位符
mysql_placeholders = ', '.join(['%s'] * placeholder_count)
# 构建 MySQL INSERT ... ON DUPLICATE KEY UPDATE 语句
mysql_sql = f"""
INSERT INTO {target_table} ({columns})
VALUES ({mysql_placeholders})
ON DUPLICATE KEY UPDATE {update_column} = VALUES({update_column})
""".strip()
return mysql_sql
def _translate_sql(self, sql: str) -> str:
"""
将 SQL Server SQL 转换为 MySQL 兼容格式
转换顺序:
1. 表名转换 ([schema].[table] -> schema_table)
2. 占位符转换 (? -> %s)
3. MERGE 语句转换
Args:
sql: SQL 语句
Returns:
转换后的 SQL 语句
"""
result = sql
# 1. 表名转换
result = self._translate_table_name(result)
# 2. 占位符转换
result = self._translate_placeholder(result)
# 3. MERGE 语句转换
if re.match(r'\s*MERGE', sql, re.IGNORECASE):
result = self._translate_merge(sql)
# MERGE 转换已经处理了表名和占位符,所以需要重新处理
result = self._translate_table_name(result)
result = self._translate_placeholder(result)
return result
def execute_query(
self, sql: str, params: Optional[tuple] = None
) -> List[Dict[str, Any]]:
"""
执行查询语句并返回结果
Args:
sql: SQL 查询语句
params: 查询参数(可选)
Returns:
List[Dict[str, Any]]: 查询结果列表,每个元素为一行数据的字典
"""
if not self.connection:
self.connect()
# 转换 SQL
translated_sql = self._translate_sql(sql)
cursor = self.connection.cursor(dictionary=True)
try:
if params:
cursor.execute(translated_sql, params)
else:
cursor.execute(translated_sql)
# 将结果转换为字典列表
results = cursor.fetchall()
return results
except mysql.connector.Error as e:
print(f"查询执行失败: {e}")
print(f"原始 SQL: {sql}")
print(f"转换后 SQL: {translated_sql}")
if params:
print(f"参数: {params}")
raise
finally:
cursor.close()
def execute_update(self, sql: str, params: Optional[tuple] = None) -> int:
"""
执行更新/插入/删除语句
Args:
sql: SQL 语句
params: 参数(可选)
Returns:
int: 受影响的行数
"""
if not self.connection:
self.connect()
# 转换 SQL
translated_sql = self._translate_sql(sql)
cursor = self.connection.cursor()
try:
if params:
cursor.execute(translated_sql, params)
else:
cursor.execute(translated_sql)
self.connection.commit()
return cursor.rowcount
except mysql.connector.Error as e:
self.connection.rollback()
print(f"执行失败,已回滚: {e}")
print(f"原始 SQL: {sql}")
print(f"转换后 SQL: {translated_sql}")
if params:
print(f"参数: {params}")
raise
finally:
cursor.close()
def __enter__(self):
"""支持 with 语句的上下文管理器入口"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""支持 with 语句的上下文管理器出口"""
self.disconnect()