RAG/sync/mysql_sync.py

284 lines
10 KiB
Python

"""MySQL synchronization implementation"""
import urllib
import pymysql
import requests
from typing import List, Dict, Optional, Tuple, Any, Set
from datetime import datetime
from loguru import logger
from config import DatabaseDataSourceConfig as DatabaseConfig, settings
from rag.file_parser import FileParser
from sync.base_sync import BaseSync
class MySQLSync(BaseSync):
"""Handle synchronization between MySQL and ChromaDB"""
def __init__(self, db_config: DatabaseConfig, vector_store_manager=None):
"""
Initialize MySQL sync with database configuration
Args:
db_config: DatabaseConfig object containing database table info
"""
super().__init__(db_config)
self.db_config = self.config
self.vector_store_manager = vector_store_manager
self.file_parser = FileParser()
self.connection = None
def _connect(self):
"""Create MySQL connection"""
# Get connection parameters from config or use defaults
host = self.db_config.host or settings.HOST
port = self.db_config.port or settings.PORT
user = self.db_config.user or settings.USER
password = self.db_config.password or settings.PASSWORD
# Log connection info (without password)
logger.info(f"Connecting to MySQL database: {self.db_config.database}")
logger.debug(f"MySQL connection details: host={host}, port={port}, user={user}")
# Create connection
self.connection = pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=self.db_config.database
)
def fetch_all_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
"""
Fetch documents from the MySQL database
Args:
last_sync_time: Last synchronization time (for incremental sync)
Returns:
List of documents
"""
self._connect()
cursor = self.connection.cursor(pymysql.cursors.DictCursor)
try:
# Build query with all columns
columns = [
self.db_config.id_column,
self.db_config.title_column,
self.db_config.content_column,
self.db_config.file_column,
self.db_config.updated_at_column
] if self.db_config.updated_at_column else [
self.db_config.id_column,
self.db_config.title_column,
self.db_config.content_column,
self.db_config.file_column
]
# Remove duplicates and None values
columns = list(set([col for col in columns if col]))
# Build the query
query = f"SELECT {', '.join(columns)} FROM {self.db_config.table_name}"
# Add incremental sync condition if applicable
params = []
if last_sync_time and self.db_config.updated_at_column:
query += f" WHERE {self.db_config.updated_at_column} > %s"
params.append(last_sync_time)
logger.debug(f"MySQL query: {query}, params: {params}")
cursor.execute(query, params)
# Parse results
documents = []
for row in cursor.fetchall():
# Handle file content if file_column is specified
if self.db_config.file_column and row.get(self.db_config.file_column):
# Extract file path from the file column
file_path = row[self.db_config.file_column]
# Load file content if file source is configured
if self.db_config.file_source_type:
file_content = self._load_file_content(file_path)
if file_content:
row[self.db_config.content_column] = file_content
# Generate unique document ID
record_id = str(row[self.db_config.id_column])
doc_id = self.generate_doc_id(record_id)
row['id'] = doc_id
# Check if document has already been synced
if self.vector_store_manager and self.vector_store_manager.document_exists(doc_id + "_chunk_0"):
# If document is already synced, check if it's been modified since last sync
if last_sync_time and self.db_config.updated_at_column and row.get(self.db_config.updated_at_column):
# Skip if not modified since last sync
if row[self.db_config.updated_at_column] <= last_sync_time:
continue
elif last_sync_time:
# No updated_at column, skip since we can't determine if it's been modified
continue
documents.append(row)
return documents
finally:
cursor.close()
def fetch_new_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
"""
Fetch new/updated documents from the MySQL database since last sync time
Args:
last_sync_time: Last synchronization time
Returns:
List of new/updated documents
"""
# Call fetch_all_documents which now handles document existence checks
return self.fetch_all_documents(last_sync_time)
def get_synced_document_ids(self) -> Set[str]:
"""
Get IDs of all documents in the MySQL database
Returns:
Set of document IDs
"""
cursor = self.connection.cursor(pymysql.cursors.DictCursor)
try:
query = f"SELECT {self.db_config.id_column} FROM {self.db_config.table_name}"
cursor.execute(query)
return {str(row[self.db_config.id_column]) for row in cursor.fetchall()}
finally:
cursor.close()
def generate_doc_id(self, record_id: str) -> str:
"""
Generate a unique document ID for MySQL records
Args:
record_id: ID of the record in the database
Returns:
Unique document ID based on database, table, and record ID
"""
# 为 MySQL 记录生成唯一的文档 ID
return f"{self.db_config.database}_{self.db_config.table_name}_{record_id}"
def doc_to_llamaindex_doc(self, doc: Dict) -> 'Document':
"""
Convert MySQL document to LlamaIndex Document
Args:
doc: MySQL document dictionary
Returns:
LlamaIndex Document object
"""
from llama_index.core import Document
# 处理多个 content 列(支持合并多个列的内容)
if self.db_config:
# 使用配置的多个 content 列
content_columns = self.db_config.content_columns
content_separator = self.db_config.content_separator
else:
# 向后兼容:使用单个 content_column
content_columns = ["content"] # Default to "content" column
content_separator = "\n"
# 合并所有 content 列的内容
content_parts = []
for col in content_columns:
col_value = doc.get(col, "")
if col_value:
content_parts.append(str(col_value))
# 用指定的分隔符连接多个列的内容
if content_parts:
if content_separator:
content = content_separator.join(content_parts)
else:
content = " ".join(content_parts) # 如果没有指定分隔符,使用空格
else:
content = ""
title = doc.get('title', "")
doc_id = doc.get('id', "")
# Build metadata
metadata = {
"doc_id": doc_id,
"source": "mysql",
"database": self.db_config.database,
"table": self.db_config.table_name
}
if title:
metadata["title"] = title
# Create Document
return Document(
text=content,
id_=doc_id,
metadata=metadata
)
@staticmethod
def check_data_source_exists(config: DatabaseConfig) -> bool:
"""
Check if the MySQL database exists and is accessible
Args:
config: Database configuration
Returns:
True if database exists and is accessible, False otherwise
"""
temp_connection = None
try:
# Get connection parameters
host = config.mysql_host or settings.MYSQL_HOST
port = config.mysql_port or settings.MYSQL_PORT
user = config.mysql_user or settings.MYSQL_USER
password = config.mysql_password or settings.MYSQL_PASSWORD
# Create connection
temp_connection = pymysql.connect(
host=host,
port=port,
user=user,
password=password
)
# Check if database exists
cursor = temp_connection.cursor()
cursor.execute(f"SHOW DATABASES LIKE '{config.database}'")
result = cursor.fetchone()
cursor.close()
if not result:
logger.error(f"Database {config.database} does not exist")
return False
# Check if table exists
temp_connection.select_db(config.database)
cursor = temp_connection.cursor()
cursor.execute(f"SHOW TABLES LIKE '{config.table_name}'")
result = cursor.fetchone()
cursor.close()
if not result:
logger.error(f"Table {config.table_name} does not exist in database {config.database}")
return False
return True
except Exception as e:
logger.error(f"Error checking MySQL data source: {e}")
return False
finally:
if temp_connection:
temp_connection.close()