463 lines
16 KiB
Python
463 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
G3FO Database Operations Tool
|
||
|
||
替代 MCP MySQL,提供完整的数据库操作能力:
|
||
- 增删改查 (query, execute)
|
||
- 列举所有表 (list_tables)
|
||
- 执行存储过程 (call_procedure)
|
||
- 批量执行 SQL (batch_execute)
|
||
- 读取表结构 (describe_table)
|
||
- 读取/修改表和列注释 (get/set comments)
|
||
|
||
默认连接配置与 MCP user-mysql 一致,可通过参数或环境变量覆盖。
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
from typing import Any, Optional
|
||
|
||
try:
|
||
import mysql.connector
|
||
from mysql.connector import Error as MySQLError
|
||
except ImportError:
|
||
print("Error: mysql-connector-python is required. Run: pip install mysql-connector-python", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
# 默认连接配置(与 MCP user-mysql 一致)
|
||
DEFAULT_CONFIG = {
|
||
"host": "192.168.3.233",
|
||
"port": 3306,
|
||
"user": "root",
|
||
"password": "afe123456",
|
||
"database": "g3fo_base",
|
||
"connection_timeout": 10, # 连接超时(秒),防止长时间挂起
|
||
}
|
||
|
||
ENV_MAPPING = {
|
||
"host": "MYSQL_HOST",
|
||
"port": "MYSQL_PORT",
|
||
"user": "MYSQL_USER",
|
||
"password": "MYSQL_PASSWORD",
|
||
"database": "MYSQL_DATABASE",
|
||
"connection_timeout": "MYSQL_CONNECTION_TIMEOUT",
|
||
}
|
||
|
||
|
||
def get_connection_config(
|
||
host: Optional[str] = None,
|
||
port: Optional[int] = None,
|
||
user: Optional[str] = None,
|
||
password: Optional[str] = None,
|
||
database: Optional[str] = None,
|
||
connection_timeout: Optional[int] = None,
|
||
) -> dict:
|
||
"""从默认配置、环境变量、参数中合并连接配置,参数优先级最高。"""
|
||
config = {}
|
||
params = {"host": host, "port": port, "user": user, "password": password, "database": database, "connection_timeout": connection_timeout}
|
||
for key, default in DEFAULT_CONFIG.items():
|
||
env_key = ENV_MAPPING.get(key)
|
||
env_val = os.environ.get(env_key) if env_key else None
|
||
param_val = params.get(key)
|
||
if param_val is not None:
|
||
config[key] = int(param_val) if key in ("port", "connection_timeout") else param_val
|
||
elif env_val is not None:
|
||
config[key] = int(env_val) if key in ("port", "connection_timeout") else env_val
|
||
else:
|
||
config[key] = default
|
||
return config
|
||
|
||
|
||
def get_connection(config: dict):
|
||
"""
|
||
创建数据库连接。
|
||
连接仅在单次命令执行期间存在,命令结束或异常时在 finally 中关闭,不会长期占用。
|
||
"""
|
||
return mysql.connector.connect(
|
||
host=config["host"],
|
||
port=config["port"],
|
||
user=config["user"],
|
||
password=config["password"],
|
||
database=config["database"],
|
||
charset="utf8mb4",
|
||
collation="utf8mb4_unicode_ci",
|
||
connection_timeout=config.get("connection_timeout", 10),
|
||
)
|
||
|
||
|
||
def cmd_query(args: argparse.Namespace, config: dict) -> dict:
|
||
"""执行 SELECT 查询。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor(dictionary=True)
|
||
cursor.execute(args.sql, args.params or [])
|
||
rows = cursor.fetchall()
|
||
cursor.close()
|
||
return {"success": True, "data": rows, "rowCount": len(rows)}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_execute(args: argparse.Namespace, config: dict) -> dict:
|
||
"""执行 INSERT/UPDATE/DELETE。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute(args.sql, args.params or [])
|
||
conn.commit()
|
||
affected = cursor.rowcount
|
||
cursor.close()
|
||
return {"success": True, "affectedRows": affected}
|
||
except MySQLError as e:
|
||
conn.rollback()
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_list_tables(args: argparse.Namespace, config: dict) -> dict:
|
||
"""列举数据库中的所有表。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, TABLE_COMMENT "
|
||
"FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME",
|
||
(config["database"],),
|
||
)
|
||
rows = cursor.fetchall()
|
||
cursor.close()
|
||
tables = [
|
||
{
|
||
"schema": r[0],
|
||
"name": r[1],
|
||
"type": r[2],
|
||
"comment": r[3] or "",
|
||
}
|
||
for r in rows
|
||
]
|
||
return {"success": True, "tables": tables, "count": len(tables)}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_describe_table(args: argparse.Namespace, config: dict) -> dict:
|
||
"""获取表结构(列信息)。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
table = args.table
|
||
schema = args.schema or config["database"]
|
||
cursor.execute(
|
||
"""
|
||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, COLUMN_COMMENT,
|
||
EXTRA
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
||
ORDER BY ORDINAL_POSITION
|
||
""",
|
||
(schema, table),
|
||
)
|
||
rows = cursor.fetchall()
|
||
cursor.close()
|
||
columns = [
|
||
{
|
||
"name": r[0],
|
||
"type": r[1],
|
||
"nullable": r[2],
|
||
"key": r[3] or "",
|
||
"default": r[4],
|
||
"comment": r[5] or "",
|
||
"extra": r[6] or "",
|
||
}
|
||
for r in rows
|
||
]
|
||
# 获取表注释
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||
(schema, table),
|
||
)
|
||
tbl = cursor.fetchone()
|
||
table_comment = tbl[0] if tbl else ""
|
||
cursor.close()
|
||
return {"success": True, "table": table, "schema": schema, "columns": columns, "tableComment": table_comment}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_call_procedure(args: argparse.Namespace, config: dict) -> dict:
|
||
"""执行存储过程。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
placeholders = ", ".join(["%s"] * len(args.params)) if args.params else ""
|
||
sql = f"CALL {args.procedure}({placeholders})" if placeholders else f"CALL {args.procedure}()"
|
||
cursor.execute(sql, args.params or [])
|
||
rows = []
|
||
if cursor.description:
|
||
rows = cursor.fetchall()
|
||
while cursor.nextset():
|
||
if cursor.description:
|
||
rows.extend(cursor.fetchall())
|
||
conn.commit()
|
||
cursor.close()
|
||
return {"success": True, "data": rows, "rowCount": len(rows)}
|
||
except MySQLError as e:
|
||
conn.rollback()
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_batch_execute(args: argparse.Namespace, config: dict) -> dict:
|
||
"""批量执行 SQL(支持多条语句、存储过程、DELIMITER)。"""
|
||
sql_text = args.sql or ""
|
||
if args.file:
|
||
with open(args.file, "r", encoding="utf-8") as f:
|
||
sql_text = f.read()
|
||
if not sql_text.strip():
|
||
return {"success": False, "error": "No SQL provided. Use 'sql' argument or --file"}
|
||
# 处理 DELIMITER(存储过程等):移除 DELIMITER 行,将 $$ 替换为 ;
|
||
if "DELIMITER" in sql_text.upper():
|
||
lines = []
|
||
for line in sql_text.split("\n"):
|
||
if line.strip().upper().startswith("DELIMITER"):
|
||
continue
|
||
lines.append(line.replace("$$", ";"))
|
||
sql_text = "\n".join(lines)
|
||
conn = get_connection(config)
|
||
results = []
|
||
try:
|
||
cursor = conn.cursor()
|
||
# mysql-connector-python 9.2+ 已移除 multi 参数,直接 execute 即可执行多条语句
|
||
cursor.execute(sql_text)
|
||
# 消费所有结果集(SELECT 返回行、DML 返回 affected rows)
|
||
while True:
|
||
if cursor.description:
|
||
results.append({"type": "query", "rowCount": len(cursor.fetchall())})
|
||
else:
|
||
results.append({"type": "execute", "affectedRows": cursor.rowcount})
|
||
if not cursor.nextset():
|
||
break
|
||
conn.commit()
|
||
cursor.close()
|
||
return {"success": True, "results": results}
|
||
except MySQLError as e:
|
||
conn.rollback()
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_get_table_comment(args: argparse.Namespace, config: dict) -> dict:
|
||
"""获取表注释。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
schema = args.schema or config["database"]
|
||
cursor.execute(
|
||
"SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||
(schema, args.table),
|
||
)
|
||
row = cursor.fetchone()
|
||
cursor.close()
|
||
return {"success": True, "table": args.table, "schema": schema, "comment": row[0] if row else ""}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_set_table_comment(args: argparse.Namespace, config: dict) -> dict:
|
||
"""修改表注释。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
schema = args.schema or config["database"]
|
||
sql = f"ALTER TABLE `{schema}`.`{args.table}` COMMENT = %s"
|
||
cursor.execute(sql, (args.comment,))
|
||
conn.commit()
|
||
cursor.close()
|
||
return {"success": True, "table": args.table, "schema": schema}
|
||
except MySQLError as e:
|
||
conn.rollback()
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_get_column_comment(args: argparse.Namespace, config: dict) -> dict:
|
||
"""获取列注释。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
schema = args.schema or config["database"]
|
||
cursor.execute(
|
||
"SELECT COLUMN_COMMENT FROM information_schema.COLUMNS "
|
||
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
|
||
(schema, args.table, args.column),
|
||
)
|
||
row = cursor.fetchone()
|
||
cursor.close()
|
||
return {"success": True, "table": args.table, "column": args.column, "comment": row[0] if row else ""}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_set_column_comment(args: argparse.Namespace, config: dict) -> dict:
|
||
"""修改列注释(需要 COLUMN_TYPE,可通过 describe_table 获取)。"""
|
||
conn = get_connection(config)
|
||
try:
|
||
cursor = conn.cursor()
|
||
schema = args.schema or config["database"]
|
||
cursor.execute(
|
||
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS "
|
||
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
|
||
(schema, args.table, args.column),
|
||
)
|
||
row = cursor.fetchone()
|
||
if not row:
|
||
cursor.close()
|
||
return {"success": False, "error": f"Column {args.column} not found"}
|
||
col_type = row[0]
|
||
sql = f"ALTER TABLE `{schema}`.`{args.table}` MODIFY COLUMN `{args.column}` {col_type} COMMENT %s"
|
||
cursor.execute(sql, (args.comment,))
|
||
conn.commit()
|
||
cursor.close()
|
||
return {"success": True, "table": args.table, "column": args.column}
|
||
except MySQLError as e:
|
||
conn.rollback()
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def cmd_test_connection(args: argparse.Namespace, config: dict) -> dict:
|
||
"""测试数据库连接。"""
|
||
conn = None
|
||
try:
|
||
conn = get_connection(config)
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT 1 AS conn_ok")
|
||
row = cursor.fetchone()
|
||
cursor.close()
|
||
return {"success": True, "message": "Connection OK", "conn_ok": row[0] if row else 1}
|
||
except MySQLError as e:
|
||
return {"success": False, "error": str(e)}
|
||
finally:
|
||
if conn:
|
||
conn.close()
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="G3FO Database Operations Tool")
|
||
parser.add_argument("--host", help="MySQL host (default: 192.168.3.233)")
|
||
parser.add_argument("--port", type=int, help="MySQL port (default: 3306)")
|
||
parser.add_argument("--user", help="MySQL user (default: root)")
|
||
parser.add_argument("--password", help="MySQL password")
|
||
parser.add_argument("--database", help="MySQL database (default: g3fo_base)")
|
||
parser.add_argument("--connection-timeout", type=int, dest="connection_timeout", help="Connection timeout in seconds (default: 10)")
|
||
|
||
|
||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||
|
||
# query
|
||
p_query = subparsers.add_parser("query", help="Execute SELECT query")
|
||
p_query.add_argument("sql", help="SQL SELECT statement")
|
||
p_query.add_argument("--params", nargs="*", help="Query parameters")
|
||
|
||
# execute
|
||
p_execute = subparsers.add_parser("execute", help="Execute INSERT/UPDATE/DELETE")
|
||
p_execute.add_argument("sql", help="SQL statement")
|
||
p_execute.add_argument("--params", nargs="*", help="Query parameters")
|
||
|
||
# list_tables
|
||
subparsers.add_parser("list_tables", help="List all tables in database")
|
||
|
||
# describe_table
|
||
p_desc = subparsers.add_parser("describe_table", help="Get table structure")
|
||
p_desc.add_argument("table", help="Table name")
|
||
p_desc.add_argument("--schema", help="Schema/database (default: current database)")
|
||
|
||
# call_procedure
|
||
p_proc = subparsers.add_parser("call_procedure", help="Execute stored procedure")
|
||
p_proc.add_argument("procedure", help="Procedure name (e.g. InsertSystemMenu)")
|
||
p_proc.add_argument("--params", nargs="*", help="Procedure parameters")
|
||
|
||
# batch_execute
|
||
p_batch = subparsers.add_parser("batch_execute", help="Execute batch SQL")
|
||
p_batch.add_argument("sql", nargs="?", help="SQL text (multiple statements separated by ;)")
|
||
p_batch.add_argument("--file", "-f", help="SQL file path")
|
||
p_batch.add_argument("--continue-on-error", action="store_true", dest="continue_on_error", help="Continue on error")
|
||
|
||
# get_table_comment
|
||
p_gtc = subparsers.add_parser("get_table_comment", help="Get table comment")
|
||
p_gtc.add_argument("table", help="Table name")
|
||
p_gtc.add_argument("--schema", help="Schema (default: current database)")
|
||
|
||
# set_table_comment
|
||
p_stc = subparsers.add_parser("set_table_comment", help="Set table comment")
|
||
p_stc.add_argument("table", help="Table name")
|
||
p_stc.add_argument("comment", help="Comment text")
|
||
p_stc.add_argument("--schema", help="Schema (default: current database)")
|
||
|
||
# get_column_comment
|
||
p_gcc = subparsers.add_parser("get_column_comment", help="Get column comment")
|
||
p_gcc.add_argument("table", help="Table name")
|
||
p_gcc.add_argument("column", help="Column name")
|
||
p_gcc.add_argument("--schema", help="Schema (default: current database)")
|
||
|
||
# set_column_comment
|
||
p_scc = subparsers.add_parser("set_column_comment", help="Set column comment")
|
||
p_scc.add_argument("table", help="Table name")
|
||
p_scc.add_argument("column", help="Column name")
|
||
p_scc.add_argument("comment", help="Comment text")
|
||
p_scc.add_argument("--schema", help="Schema (default: current database)")
|
||
|
||
# test_connection
|
||
subparsers.add_parser("test_connection", help="Test database connection")
|
||
|
||
args = parser.parse_args()
|
||
config = get_connection_config(
|
||
host=args.host,
|
||
port=args.port,
|
||
user=args.user,
|
||
password=args.password,
|
||
database=args.database,
|
||
connection_timeout=getattr(args, "connection_timeout", None),
|
||
)
|
||
|
||
commands = {
|
||
"query": cmd_query,
|
||
"execute": cmd_execute,
|
||
"list_tables": cmd_list_tables,
|
||
"describe_table": cmd_describe_table,
|
||
"call_procedure": cmd_call_procedure,
|
||
"batch_execute": cmd_batch_execute,
|
||
"get_table_comment": cmd_get_table_comment,
|
||
"set_table_comment": cmd_set_table_comment,
|
||
"get_column_comment": cmd_get_column_comment,
|
||
"set_column_comment": cmd_set_column_comment,
|
||
"test_connection": cmd_test_connection,
|
||
}
|
||
handler = commands[args.command]
|
||
result = handler(args, config)
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
sys.exit(0 if result.get("success", False) else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|