Coverage for src\core\storage.py: 0%
303 statements
« prev ^ index » next coverage.py v7.3.4, created at 2026-04-21 14:54 +0800
« prev ^ index » next coverage.py v7.3.4, created at 2026-04-21 14:54 +0800
1"""
2数据存储系统
3提供统一的存储接口,支持多种存储后端
4"""
6import json
7import sqlite3
8import aiofiles
9from pathlib import Path
10from typing import Dict, List, Optional, Any, Union, Tuple
11from enum import Enum
12import asyncio
13from dataclasses import dataclass, asdict
14from datetime import datetime
15import time
18class StorageType(str, Enum):
19 """存储类型"""
20 SQLITE = "sqlite"
21 JSON_FILE = "json"
22 MEMORY = "memory"
25@dataclass
26class StorageConfig:
27 """存储配置"""
29 type: StorageType = StorageType.SQLITE
30 path: Optional[Path] = None
31 table_name: str = "memories"
32 max_connections: int = 5
33 auto_commit: bool = True
36class StorageBackend:
37 """存储后端基类"""
39 async def connect(self) -> None:
40 """连接到存储"""
41 pass
43 async def disconnect(self) -> None:
44 """断开连接"""
45 pass
47 async def save(self, key: str, data: Dict[str, Any]) -> bool:
48 """保存数据"""
49 raise NotImplementedError
51 async def load(self, key: str) -> Optional[Dict[str, Any]]:
52 """加载数据"""
53 raise NotImplementedError
55 async def delete(self, key: str) -> bool:
56 """删除数据"""
57 raise NotImplementedError
59 async def query(self, conditions: Dict[str, Any], limit: int = 100) -> List[Dict[str, Any]]:
60 """查询数据"""
61 raise NotImplementedError
63 async def exists(self, key: str) -> bool:
64 """检查数据是否存在"""
65 raise NotImplementedError
67 async def list_keys(self, prefix: str = "") -> List[str]:
68 """列出所有键"""
69 raise NotImplementedError
72class SQLiteBackend(StorageBackend):
73 """SQLite 存储后端"""
75 def __init__(self, config: StorageConfig):
76 self.config = config
77 self.connection: Optional[sqlite3.Connection] = None
79 # 确保路径存在
80 if self.config.path:
81 path = self.config.path if isinstance(self.config.path, Path) else Path(self.config.path)
82 path.parent.mkdir(parents=True, exist_ok=True)
84 async def connect(self) -> None:
85 """连接到 SQLite 数据库"""
86 if self.config.path is None:
87 raise ValueError("SQLite 存储需要指定路径")
89 # SQLite 连接是同步的,但我们在异步环境中使用
90 self.connection = sqlite3.connect(
91 str(self.config.path),
92 check_same_thread=False
93 )
95 # 启用外键和 WAL 模式
96 self.connection.execute("PRAGMA foreign_keys = ON")
97 self.connection.execute("PRAGMA journal_mode = WAL")
99 # 创建表
100 await self._create_tables()
102 async def disconnect(self) -> None:
103 """断开连接"""
104 if self.connection:
105 self.connection.close()
106 self.connection = None
108 async def save(self, key: str, data: Dict[str, Any]) -> bool:
109 """保存数据到 SQLite"""
110 if not self.connection:
111 await self.connect()
113 try:
114 # 序列化数据
115 data_json = json.dumps(data, ensure_ascii=False)
117 # 插入或更新
118 cursor = self.connection.execute(
119 f"""
120 INSERT OR REPLACE INTO {self.config.table_name}
121 (key, data, created_at, updated_at)
122 VALUES (?, ?, ?, ?)
123 """,
124 (key, data_json, time.time(), time.time())
125 )
127 if self.config.auto_commit:
128 self.connection.commit()
130 return cursor.rowcount > 0
132 except Exception as e:
133 print(f"SQLite 保存失败: {e}")
134 return False
136 async def load(self, key: str) -> Optional[Dict[str, Any]]:
137 """从 SQLite 加载数据"""
138 if not self.connection:
139 await self.connect()
141 try:
142 cursor = self.connection.execute(
143 f"SELECT data FROM {self.config.table_name} WHERE key = ?",
144 (key,)
145 )
147 row = cursor.fetchone()
148 if row:
149 return json.loads(row[0])
151 return None
153 except Exception as e:
154 print(f"SQLite 加载失败: {e}")
155 return None
157 async def delete(self, key: str) -> bool:
158 """从 SQLite 删除数据"""
159 if not self.connection:
160 await self.connect()
162 try:
163 cursor = self.connection.execute(
164 f"DELETE FROM {self.config.table_name} WHERE key = ?",
165 (key,)
166 )
168 if self.config.auto_commit:
169 self.connection.commit()
171 return cursor.rowcount > 0
173 except Exception as e:
174 print(f"SQLite 删除失败: {e}")
175 return False
177 async def query(self, conditions: Dict[str, Any], limit: int = 100) -> List[Dict[str, Any]]:
178 """查询 SQLite 数据"""
179 if not self.connection:
180 await self.connect()
182 try:
183 # 构建查询条件
184 where_clauses = []
185 params = []
187 for key, value in conditions.items():
188 where_clauses.append(f"json_extract(data, '$.{key}') = ?")
189 params.append(value)
191 where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
193 # 执行查询
194 cursor = self.connection.execute(
195 f"""
196 SELECT data FROM {self.config.table_name}
197 WHERE {where_sql}
198 ORDER BY updated_at DESC
199 LIMIT ?
200 """,
201 (*params, limit)
202 )
204 results = []
205 for row in cursor.fetchall():
206 results.append(json.loads(row[0]))
208 return results
210 except Exception as e:
211 print(f"SQLite 查询失败: {e}")
212 return []
214 async def exists(self, key: str) -> bool:
215 """检查键是否存在"""
216 if not self.connection:
217 await self.connect()
219 try:
220 cursor = self.connection.execute(
221 f"SELECT 1 FROM {self.config.table_name} WHERE key = ?",
222 (key,)
223 )
225 return cursor.fetchone() is not None
227 except Exception as e:
228 print(f"SQLite 检查存在失败: {e}")
229 return False
231 async def list_keys(self, prefix: str = "") -> List[str]:
232 """列出所有键"""
233 if not self.connection:
234 await self.connect()
236 try:
237 if prefix:
238 cursor = self.connection.execute(
239 f"SELECT key FROM {self.config.table_name} WHERE key LIKE ?",
240 (f"{prefix}%",)
241 )
242 else:
243 cursor = self.connection.execute(
244 f"SELECT key FROM {self.config.table_name}"
245 )
247 return [row[0] for row in cursor.fetchall()]
249 except Exception as e:
250 print(f"SQLite 列出键失败: {e}")
251 return []
253 async def _create_tables(self) -> None:
254 """创建数据库表"""
255 if not self.connection:
256 return
258 self.connection.execute(f"""
259 CREATE TABLE IF NOT EXISTS {self.config.table_name} (
260 key TEXT PRIMARY KEY,
261 data TEXT NOT NULL,
262 created_at REAL NOT NULL,
263 updated_at REAL NOT NULL
264 )
265 """)
267 # 创建索引
268 self.connection.execute(f"""
269 CREATE INDEX IF NOT EXISTS idx_updated_at
270 ON {self.config.table_name} (updated_at)
271 """)
273 self.connection.commit()
276class JSONFileBackend(StorageBackend):
277 """JSON 文件存储后端"""
279 def __init__(self, config: StorageConfig):
280 self.config = config
281 self.data: Dict[str, Dict[str, Any]] = {}
283 # 确保路径存在
284 if self.config.path:
285 path = self.config.path if isinstance(self.config.path, Path) else Path(self.config.path)
286 path.parent.mkdir(parents=True, exist_ok=True)
288 async def connect(self) -> None:
289 """连接到 JSON 文件存储"""
290 if self.config.path and self.config.path.exists():
291 await self._load_from_file()
293 async def disconnect(self) -> None:
294 """断开连接并保存到文件"""
295 if self.config.path:
296 await self._save_to_file()
298 async def save(self, key: str, data: Dict[str, Any]) -> bool:
299 """保存数据到 JSON 文件"""
300 try:
301 # 添加时间戳
302 if "created_at" not in data:
303 data["created_at"] = time.time()
304 data["updated_at"] = time.time()
306 # 保存到内存
307 self.data[key] = data
309 # 异步保存到文件
310 asyncio.create_task(self._save_to_file())
312 return True
314 except Exception as e:
315 print(f"JSON 文件保存失败: {e}")
316 return False
318 async def load(self, key: str) -> Optional[Dict[str, Any]]:
319 """从 JSON 文件加载数据"""
320 return self.data.get(key)
322 async def delete(self, key: str) -> bool:
323 """从 JSON 文件删除数据"""
324 try:
325 if key in self.data:
326 del self.data[key]
328 # 异步保存到文件
329 asyncio.create_task(self._save_to_file())
331 return True
333 return False
335 except Exception as e:
336 print(f"JSON 文件删除失败: {e}")
337 return False
339 async def query(self, conditions: Dict[str, Any], limit: int = 100) -> List[Dict[str, Any]]:
340 """查询 JSON 文件数据"""
341 results = []
343 for item in self.data.values():
344 match = True
346 for key, value in conditions.items():
347 if key not in item or item[key] != value:
348 match = False
349 break
351 if match:
352 results.append(item)
354 # 按更新时间排序
355 results.sort(key=lambda x: x.get("updated_at", 0), reverse=True)
357 return results[:limit]
359 async def exists(self, key: str) -> bool:
360 """检查键是否存在"""
361 return key in self.data
363 async def list_keys(self, prefix: str = "") -> List[str]:
364 """列出所有键"""
365 if prefix:
366 return [key for key in self.data.keys() if key.startswith(prefix)]
368 return list(self.data.keys())
370 async def _load_from_file(self) -> None:
371 """从文件加载数据"""
372 if not self.config.path or not self.config.path.exists():
373 return
375 try:
376 async with aiofiles.open(self.config.path, "r", encoding="utf-8") as f:
377 content = await f.read()
378 self.data = json.loads(content)
380 except Exception as e:
381 print(f"加载 JSON 文件失败: {e}")
382 self.data = {}
384 async def _save_to_file(self) -> None:
385 """保存数据到文件"""
386 if not self.config.path:
387 return
389 try:
390 async with aiofiles.open(self.config.path, "w", encoding="utf-8") as f:
391 await f.write(json.dumps(self.data, indent=2, ensure_ascii=False))
393 except Exception as e:
394 print(f"保存 JSON 文件失败: {e}")
397class MemoryBackend(StorageBackend):
398 """内存存储后端(用于测试)"""
400 def __init__(self, config: StorageConfig):
401 self.config = config
402 self.data: Dict[str, Dict[str, Any]] = {}
404 async def save(self, key: str, data: Dict[str, Any]) -> bool:
405 """保存数据到内存"""
406 try:
407 if "created_at" not in data:
408 data["created_at"] = time.time()
409 data["updated_at"] = time.time()
411 self.data[key] = data
412 return True
414 except Exception as e:
415 print(f"内存保存失败: {e}")
416 return False
418 async def load(self, key: str) -> Optional[Dict[str, Any]]:
419 """从内存加载数据"""
420 return self.data.get(key)
422 async def delete(self, key: str) -> bool:
423 """从内存删除数据"""
424 try:
425 if key in self.data:
426 del self.data[key]
427 return True
429 return False
431 except Exception as e:
432 print(f"内存删除失败: {e}")
433 return False
435 async def query(self, conditions: Dict[str, Any], limit: int = 100) -> List[Dict[str, Any]]:
436 """查询内存数据"""
437 results = []
439 for item in self.data.values():
440 match = True
442 for key, value in conditions.items():
443 if key not in item or item[key] != value:
444 match = False
445 break
447 if match:
448 results.append(item)
450 # 按更新时间排序
451 results.sort(key=lambda x: x.get("updated_at", 0), reverse=True)
453 return results[:limit]
455 async def exists(self, key: str) -> bool:
456 """检查键是否存在"""
457 return key in self.data
459 async def list_keys(self, prefix: str = "") -> List[str]:
460 """列出所有键"""
461 if prefix:
462 return [key for key in self.data.keys() if key.startswith(prefix)]
464 return list(self.data.keys())
467class StorageManager:
468 """存储管理器"""
470 def __init__(self, config: Optional[StorageConfig] = None):
471 self.config = config or StorageConfig()
472 self.backend: Optional[StorageBackend] = None
474 async def initialize(self) -> None:
475 """初始化存储"""
476 # 根据配置创建后端
477 if self.config.type == StorageType.SQLITE:
478 self.backend = SQLiteBackend(self.config)
480 elif self.config.type == StorageType.JSON_FILE:
481 self.backend = JSONFileBackend(self.config)
483 elif self.config.type == StorageType.MEMORY:
484 self.backend = MemoryBackend(self.config)
486 else:
487 raise ValueError(f"不支持的存储类型: {self.config.type}")
489 # 连接后端
490 await self.backend.connect()
492 async def close(self) -> None:
493 """关闭存储"""
494 if self.backend:
495 await self.backend.disconnect()
497 async def save(self, key: str, data: Dict[str, Any]) -> bool:
498 """保存数据"""
499 if not self.backend:
500 await self.initialize()
502 return await self.backend.save(key, data)
504 async def load(self, key: str) -> Optional[Dict[str, Any]]:
505 """加载数据"""
506 if not self.backend:
507 await self.initialize()
509 return await self.backend.load(key)
511 async def delete(self, key: str) -> bool:
512 """删除数据"""
513 if not self.backend:
514 await self.initialize()
516 return await self.backend.delete(key)
518 async def query(self, conditions: Dict[str, Any], limit: int = 100) -> List[Dict[str, Any]]:
519 """查询数据"""
520 if not self.backend:
521 await self.initialize()
523 return await self.backend.query(conditions, limit)
525 async def exists(self, key: str) -> bool:
526 """检查数据是否存在"""
527 if not self.backend:
528 await self.initialize()
530 return await self.backend.exists(key)
532 async def list_keys(self, prefix: str = "") -> List[str]:
533 """列出所有键"""
534 if not self.backend:
535 await self.initialize()
537 return await self.backend.list_keys(prefix)
539 async def migrate_data(self, source_backend: StorageBackend, target_backend: StorageBackend) -> int:
540 """
541 迁移数据
543 Args:
544 source_backend: 源存储后端
545 target_backend: 目标存储后端
547 Returns:
548 迁移的数据数量
549 """
550 migrated_count = 0
552 try:
553 # 获取所有键
554 keys = await source_backend.list_keys()
556 for key in keys:
557 # 加载数据
558 data = await source_backend.load(key)
559 if data:
560 # 保存到目标
561 if await target_backend.save(key, data):
562 migrated_count += 1
564 return migrated_count
566 except Exception as e:
567 print(f"数据迁移失败: {e}")
568 return migrated_count