- 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>
325 lines
9.7 KiB
Python
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()
|