RAG/sync/dameng_sync.py

290 lines
10 KiB
Python

"""Dameng database synchronization implementation"""
import urllib
import dmPython # 达梦数据库驱动
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 DaMengSync(BaseSync):
"""Handle synchronization between Dameng database and ChromaDB"""
def __init__(self, db_config: DatabaseConfig, vector_store_manager=None):
"""
Initialize DaMeng 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 Dameng database 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
# database = self.db_config.database or settings.DATABASE
# Log connection info (without password)
logger.info(f"Connecting to DaMeng Schema: {user}")
logger.debug(f"DaMeng connection details: host={host}, port={port}, user={user}")
# Create connection
self.connection = dmPython.connect(
user = user,
password = password,
server = host,
port = port
)
def fetch_all_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
"""
Fetch documents from the DaMeng database
Args:
last_sync_time: Last synchronization time (for incremental sync)
Returns:
List of documents
"""
self._connect()
cursor = self.connection.cursor()
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} > ?"
params.append(last_sync_time)
logger.debug(f"DaMeng query: {query}, params: {params}")
cursor.execute(query, params)
# Get column names from cursor description
column_names = [desc[0] for desc in cursor.description]
# Parse results
documents = []
for row in cursor.fetchall():
# Convert row tuple to dict
row_dict = dict(zip(column_names, row))
# Handle file content if file_column is specified
if self.db_config.file_column and row_dict.get(self.db_config.file_column):
# Extract file path from the file column
file_path = row_dict[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_dict[self.db_config.content_column] = file_content
# Generate unique document ID
record_id = str(row_dict[self.db_config.id_column])
doc_id = self.generate_doc_id(record_id)
row_dict['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_dict.get(
self.db_config.updated_at_column):
# Skip if not modified since last sync
if row_dict[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_dict)
return documents
finally:
cursor.close()
def fetch_new_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
"""
Fetch new/updated documents from the Dameng 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 Dameng database
Returns:
Set of document IDs
"""
cursor = self.connection.cursor()
try:
query = f"SELECT {self.db_config.id_column} FROM {self.db_config.table_name}"
cursor.execute(query)
column_names = [desc[0] for desc in cursor.description]
id_index = column_names.index(self.db_config.id_column)
return {str(row[id_index]) for row in cursor.fetchall()}
finally:
cursor.close()
def generate_doc_id(self, record_id: str) -> str:
"""
Generate a unique document ID for Dameng records
Args:
record_id: ID of the record in the database
Returns:
Unique document ID based on user, table, and record ID
"""
# 为达梦数据库记录生成唯一的文档 ID
return f"{self.db_config.user}_dameng_{self.db_config.table_name}_{record_id}"
def doc_to_llamaindex_doc(self, doc: Dict) -> 'Document':
"""
Convert Dameng document to LlamaIndex Document
Args:
doc: Dameng 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": "dameng",
# Dameng中没有database的概念
"schema": self.db_config.user,
"table": self.db_config.table_name,
"db_source": self.db_config.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 DaMeng table exists and is accessible
Args:
config: Database configuration
Returns:
True if table exists and is accessible, False otherwise
"""
temp_connection = None
try:
# Get connection parameters
host = config.host or settings.HOST
port = config.port or settings.PORT
user = config.user or settings.USER
password = config.password or settings.PASSWORD
# Create connection
temp_connection = dmPython.connect(
user=user,
password=password,
server=host,
port=port
)
# Check if table exists
cursor = temp_connection.cursor()
cursor.execute(
# "SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME = ?",
"SELECT COUNT(*) FROM ALL_TABLES WHERE OWNER = ? AND TABLE_NAME = ?",
[config.user.upper(), config.table_name.upper()] # DM 模式名、表名默认大写
)
result = cursor.fetchone()
cursor.close()
# 判断查询结果
if result and result[0] > 0:
return True # 表存在
else:
logger.error(f"Table {config.table_name} does not exist in database {config.database}")
return False # 表不存在
except Exception as e:
logger.error(f"Error checking Dameng data source: {e}")
return False
finally:
if temp_connection:
temp_connection.close()