db_client_toolkit 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- db_client_toolkit-0.0.1.dist-info/METADATA +540 -0
- db_client_toolkit-0.0.1.dist-info/RECORD +22 -0
- db_client_toolkit-0.0.1.dist-info/WHEEL +4 -0
- db_toolkit/__init__.py +151 -0
- db_toolkit/clients/__init__.py +20 -0
- db_toolkit/clients/mongodb.py +251 -0
- db_toolkit/clients/mysql.py +143 -0
- db_toolkit/clients/postgresql.py +152 -0
- db_toolkit/clients/redis.py +321 -0
- db_toolkit/clients/sqlite.py +152 -0
- db_toolkit/clients/supabase.py +230 -0
- db_toolkit/core/__init__.py +9 -0
- db_toolkit/core/base.py +194 -0
- db_toolkit/core/sql_base.py +163 -0
- db_toolkit/exceptions/__init__.py +38 -0
- db_toolkit/mixins/__init__.py +12 -0
- db_toolkit/mixins/batch_ops.py +194 -0
- db_toolkit/mixins/transaction.py +206 -0
- db_toolkit/utils/__init__.py +15 -0
- db_toolkit/utils/config.py +252 -0
- db_toolkit/utils/factory.py +172 -0
- db_toolkit/utils/query_builder.py +316 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis数据库客户端
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from ..core.base import BaseClient
|
|
10
|
+
from ..exceptions import ConnectionError, QueryError, NotSupportedError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisClient(BaseClient):
|
|
16
|
+
"""Redis数据库客户端实现"""
|
|
17
|
+
|
|
18
|
+
def _validate_config(self) -> None:
|
|
19
|
+
"""验证Redis配置"""
|
|
20
|
+
super()._validate_config()
|
|
21
|
+
# Redis的配置比较灵活,不强制要求特定字段
|
|
22
|
+
|
|
23
|
+
def connect(self) -> bool:
|
|
24
|
+
"""连接Redis数据库"""
|
|
25
|
+
try:
|
|
26
|
+
import redis
|
|
27
|
+
|
|
28
|
+
self.connection = redis.Redis(
|
|
29
|
+
host=self.config.get('host', 'localhost'),
|
|
30
|
+
port=self.config.get('port', 6379),
|
|
31
|
+
password=self.config.get('password'),
|
|
32
|
+
db=self.config.get('db', 0),
|
|
33
|
+
decode_responses=True,
|
|
34
|
+
**self.config.get('options', {})
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# 测试连接
|
|
38
|
+
self.connection.ping()
|
|
39
|
+
|
|
40
|
+
self._connected = True
|
|
41
|
+
logger.info(f"Redis连接成功: {self.config.get('host', 'localhost')}:{self.config.get('port', 6379)}")
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise ConnectionError("未安装redis库,请运行: pip install redis")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
self._connected = False
|
|
48
|
+
logger.error(f"Redis连接失败: {e}")
|
|
49
|
+
raise ConnectionError(f"Redis连接失败: {e}")
|
|
50
|
+
|
|
51
|
+
def disconnect(self) -> bool:
|
|
52
|
+
"""断开Redis连接"""
|
|
53
|
+
if self.connection:
|
|
54
|
+
try:
|
|
55
|
+
self.connection.close()
|
|
56
|
+
self._connected = False
|
|
57
|
+
logger.info("Redis连接已关闭")
|
|
58
|
+
return True
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"关闭Redis连接失败: {e}")
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def is_connected(self) -> bool:
|
|
65
|
+
"""检查Redis连接状态"""
|
|
66
|
+
if not self.connection:
|
|
67
|
+
return False
|
|
68
|
+
try:
|
|
69
|
+
self.connection.ping()
|
|
70
|
+
return True
|
|
71
|
+
except Exception:
|
|
72
|
+
self._connected = False
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def execute(self, query: str, params: Optional[tuple] = None) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Redis不支持SQL查询
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
NotSupportedError: 总是抛出此异常
|
|
81
|
+
"""
|
|
82
|
+
raise NotSupportedError("Redis不支持SQL查询,请使用专用方法")
|
|
83
|
+
|
|
84
|
+
# ============ 基础键值操作 ============
|
|
85
|
+
|
|
86
|
+
def get(self, key: str) -> Optional[str]:
|
|
87
|
+
"""
|
|
88
|
+
获取键值
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
key: 键名
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Optional[str]: 值,不存在返回None
|
|
95
|
+
"""
|
|
96
|
+
if not self.is_connected():
|
|
97
|
+
raise ConnectionError("数据库未连接")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
return self.connection.get(key)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"获取键值失败: {e}")
|
|
103
|
+
raise QueryError(f"获取键值失败: {e}")
|
|
104
|
+
|
|
105
|
+
def set(self, key: str, value: str, ex: Optional[int] = None,
|
|
106
|
+
nx: bool = False, xx: bool = False) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
设置键值
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
key: 键名
|
|
112
|
+
value: 值
|
|
113
|
+
ex: 过期时间(秒)
|
|
114
|
+
nx: 只在键不存在时设置
|
|
115
|
+
xx: 只在键存在时设置
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: 是否设置成功
|
|
119
|
+
"""
|
|
120
|
+
if not self.is_connected():
|
|
121
|
+
raise ConnectionError("数据库未连接")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
return self.connection.set(key, value, ex=ex, nx=nx, xx=xx)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"设置键值失败: {e}")
|
|
127
|
+
raise QueryError(f"设置键值失败: {e}")
|
|
128
|
+
|
|
129
|
+
def delete_key(self, *keys: str) -> int:
|
|
130
|
+
"""
|
|
131
|
+
删除键
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
*keys: 要删除的键
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
int: 删除的键数量
|
|
138
|
+
"""
|
|
139
|
+
if not self.is_connected():
|
|
140
|
+
raise ConnectionError("数据库未连接")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
return self.connection.delete(*keys)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"删除键失败: {e}")
|
|
146
|
+
raise QueryError(f"删除键失败: {e}")
|
|
147
|
+
|
|
148
|
+
def exists_key(self, *keys: str) -> int:
|
|
149
|
+
"""
|
|
150
|
+
检查键是否存在
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
*keys: 要检查的键
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
int: 存在的键数量
|
|
157
|
+
"""
|
|
158
|
+
if not self.is_connected():
|
|
159
|
+
raise ConnectionError("数据库未连接")
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
return self.connection.exists(*keys)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"检查键存在失败: {e}")
|
|
165
|
+
raise QueryError(f"检查键存在失败: {e}")
|
|
166
|
+
|
|
167
|
+
# ============ Hash操作 ============
|
|
168
|
+
|
|
169
|
+
def hget(self, name: str, key: str) -> Optional[str]:
|
|
170
|
+
"""获取Hash字段值"""
|
|
171
|
+
if not self.is_connected():
|
|
172
|
+
raise ConnectionError("数据库未连接")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
return self.connection.hget(name, key)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"获取Hash字段失败: {e}")
|
|
178
|
+
raise QueryError(f"获取Hash字段失败: {e}")
|
|
179
|
+
|
|
180
|
+
def hset(self, name: str, key: str = None, value: str = None,
|
|
181
|
+
mapping: Dict[str, Any] = None) -> int:
|
|
182
|
+
"""设置Hash字段"""
|
|
183
|
+
if not self.is_connected():
|
|
184
|
+
raise ConnectionError("数据库未连接")
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
if mapping:
|
|
188
|
+
return self.connection.hset(name, mapping=mapping)
|
|
189
|
+
else:
|
|
190
|
+
return self.connection.hset(name, key, value)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"设置Hash字段失败: {e}")
|
|
193
|
+
raise QueryError(f"设置Hash字段失败: {e}")
|
|
194
|
+
|
|
195
|
+
def hgetall(self, name: str) -> Dict[str, str]:
|
|
196
|
+
"""获取Hash所有字段"""
|
|
197
|
+
if not self.is_connected():
|
|
198
|
+
raise ConnectionError("数据库未连接")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
return self.connection.hgetall(name)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"获取Hash所有字段失败: {e}")
|
|
204
|
+
raise QueryError(f"获取Hash所有字段失败: {e}")
|
|
205
|
+
|
|
206
|
+
# ============ 实现基类抽象方法(使用Hash存储) ============
|
|
207
|
+
|
|
208
|
+
def insert(self, table: str, data: Dict[str, Any]) -> Any:
|
|
209
|
+
"""
|
|
210
|
+
使用Hash存储数据
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
table: 表名(用作键前缀)
|
|
214
|
+
data: 数据,必须包含'id'字段
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
str: 键名
|
|
218
|
+
"""
|
|
219
|
+
if not self.is_connected():
|
|
220
|
+
raise ConnectionError("数据库未连接")
|
|
221
|
+
|
|
222
|
+
# 获取或生成ID
|
|
223
|
+
if 'id' in data:
|
|
224
|
+
record_id = data['id']
|
|
225
|
+
else:
|
|
226
|
+
record_id = self.connection.incr(f'{table}:id')
|
|
227
|
+
data['id'] = str(record_id)
|
|
228
|
+
|
|
229
|
+
key = f"{table}:{record_id}"
|
|
230
|
+
|
|
231
|
+
# 将数据转换为字符串存储
|
|
232
|
+
str_data = {k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
|
|
233
|
+
for k, v in data.items()}
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
self.hset(key, mapping=str_data)
|
|
237
|
+
logger.debug(f"插入数据成功,键: {key}")
|
|
238
|
+
return key
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"插入数据失败: {e}")
|
|
241
|
+
raise QueryError(f"插入数据失败: {e}")
|
|
242
|
+
|
|
243
|
+
def update(self, table: str, data: Dict[str, Any],
|
|
244
|
+
condition: Dict[str, Any]) -> int:
|
|
245
|
+
"""更新Hash数据"""
|
|
246
|
+
if not self.is_connected():
|
|
247
|
+
raise ConnectionError("数据库未连接")
|
|
248
|
+
|
|
249
|
+
if 'id' not in condition:
|
|
250
|
+
raise ValueError("Redis更新操作需要提供id条件")
|
|
251
|
+
|
|
252
|
+
key = f"{table}:{condition['id']}"
|
|
253
|
+
|
|
254
|
+
if not self.exists_key(key):
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
# 转换数据
|
|
258
|
+
str_data = {k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
|
|
259
|
+
for k, v in data.items()}
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
self.hset(key, mapping=str_data)
|
|
263
|
+
logger.debug(f"更新数据成功,键: {key}")
|
|
264
|
+
return 1
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"更新数据失败: {e}")
|
|
267
|
+
raise QueryError(f"更新数据失败: {e}")
|
|
268
|
+
|
|
269
|
+
def delete(self, table: str, condition: Dict[str, Any]) -> int:
|
|
270
|
+
"""删除Hash数据"""
|
|
271
|
+
if not self.is_connected():
|
|
272
|
+
raise ConnectionError("数据库未连接")
|
|
273
|
+
|
|
274
|
+
if 'id' not in condition:
|
|
275
|
+
raise ValueError("Redis删除操作需要提供id条件")
|
|
276
|
+
|
|
277
|
+
key = f"{table}:{condition['id']}"
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
count = self.delete_key(key)
|
|
281
|
+
logger.debug(f"删除了 {count} 条数据")
|
|
282
|
+
return count
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"删除数据失败: {e}")
|
|
285
|
+
raise QueryError(f"删除数据失败: {e}")
|
|
286
|
+
|
|
287
|
+
def select(self, table: str,
|
|
288
|
+
fields: Optional[List[str]] = None,
|
|
289
|
+
condition: Optional[Dict[str, Any]] = None,
|
|
290
|
+
limit: Optional[int] = None,
|
|
291
|
+
offset: Optional[int] = None,
|
|
292
|
+
order_by: Optional[List[tuple]] = None) -> List[Dict]:
|
|
293
|
+
"""查询Hash数据"""
|
|
294
|
+
if not self.is_connected():
|
|
295
|
+
raise ConnectionError("数据库未连接")
|
|
296
|
+
|
|
297
|
+
# Redis简化实现,只支持通过id查询
|
|
298
|
+
if not condition or 'id' not in condition:
|
|
299
|
+
logger.warning("Redis select操作建议提供id条件以提高性能")
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
key = f"{table}:{condition['id']}"
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
data = self.hgetall(key)
|
|
306
|
+
if not data:
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
# 尝试解析JSON
|
|
310
|
+
result = {}
|
|
311
|
+
for k, v in data.items():
|
|
312
|
+
try:
|
|
313
|
+
result[k] = json.loads(v)
|
|
314
|
+
except (json.JSONDecodeError, ValueError):
|
|
315
|
+
result[k] = v
|
|
316
|
+
|
|
317
|
+
return [result]
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.error(f"查询数据失败: {e}")
|
|
321
|
+
raise QueryError(f"查询数据失败: {e}")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite数据库客户端
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
import logging
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
from ..core.sql_base import SQLBaseClient
|
|
10
|
+
from ..exceptions import ConnectionError, QueryError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SQLiteClient(SQLBaseClient):
|
|
16
|
+
"""SQLite数据库客户端实现"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def placeholder(self) -> str:
|
|
20
|
+
return "?"
|
|
21
|
+
|
|
22
|
+
def _validate_config(self) -> None:
|
|
23
|
+
"""验证SQLite配置"""
|
|
24
|
+
super()._validate_config()
|
|
25
|
+
if 'database' not in self.config:
|
|
26
|
+
raise ValueError("SQLite配置缺少必需字段: database")
|
|
27
|
+
|
|
28
|
+
def connect(self) -> bool:
|
|
29
|
+
"""连接SQLite数据库"""
|
|
30
|
+
try:
|
|
31
|
+
database = self.config.get('database', ':memory:')
|
|
32
|
+
self.connection = sqlite3.connect(database)
|
|
33
|
+
self.connection.row_factory = sqlite3.Row
|
|
34
|
+
self._connected = True
|
|
35
|
+
logger.info(f"SQLite连接成功: {database}")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self._connected = False
|
|
40
|
+
logger.error(f"SQLite连接失败: {e}")
|
|
41
|
+
raise ConnectionError(f"SQLite连接失败: {e}")
|
|
42
|
+
|
|
43
|
+
def disconnect(self) -> bool:
|
|
44
|
+
"""断开SQLite连接"""
|
|
45
|
+
if self.connection:
|
|
46
|
+
try:
|
|
47
|
+
self.connection.close()
|
|
48
|
+
self._connected = False
|
|
49
|
+
logger.info("SQLite连接已关闭")
|
|
50
|
+
return True
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"关闭SQLite连接失败: {e}")
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def is_connected(self) -> bool:
|
|
57
|
+
"""检查SQLite连接状态"""
|
|
58
|
+
if not self.connection:
|
|
59
|
+
return False
|
|
60
|
+
try:
|
|
61
|
+
# 尝试执行一个简单的查询
|
|
62
|
+
self.connection.execute("SELECT 1")
|
|
63
|
+
return True
|
|
64
|
+
except Exception:
|
|
65
|
+
self._connected = False
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def execute(self, query: str, params: Optional[tuple] = None) -> Any:
|
|
69
|
+
"""执行SQLite查询"""
|
|
70
|
+
if not self.is_connected():
|
|
71
|
+
raise ConnectionError("数据库未连接")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
cursor = self.connection.cursor()
|
|
75
|
+
cursor.execute(query, params or ())
|
|
76
|
+
|
|
77
|
+
if query.strip().upper().startswith('SELECT'):
|
|
78
|
+
result = [dict(row) for row in cursor.fetchall()]
|
|
79
|
+
else:
|
|
80
|
+
self.connection.commit()
|
|
81
|
+
result = cursor.rowcount
|
|
82
|
+
|
|
83
|
+
cursor.close()
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"查询执行失败: {e}")
|
|
88
|
+
raise QueryError(f"查询执行失败: {e}")
|
|
89
|
+
|
|
90
|
+
def insert(self, table: str, data: Dict[str, Any]) -> Any:
|
|
91
|
+
"""插入数据到SQLite"""
|
|
92
|
+
query, params = self._build_insert_query(table, data)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
cursor = self.connection.cursor()
|
|
96
|
+
cursor.execute(query, params)
|
|
97
|
+
self.connection.commit()
|
|
98
|
+
last_id = cursor.lastrowid
|
|
99
|
+
cursor.close()
|
|
100
|
+
logger.debug(f"插入数据成功,ID: {last_id}")
|
|
101
|
+
return last_id
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"插入数据失败: {e}")
|
|
105
|
+
raise QueryError(f"插入数据失败: {e}")
|
|
106
|
+
|
|
107
|
+
def update(self, table: str, data: Dict[str, Any],
|
|
108
|
+
condition: Dict[str, Any]) -> int:
|
|
109
|
+
"""更新SQLite数据"""
|
|
110
|
+
query, params = self._build_update_query(table, data, condition)
|
|
111
|
+
result = self.execute(query, params)
|
|
112
|
+
logger.debug(f"更新了 {result} 条记录")
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
def delete(self, table: str, condition: Dict[str, Any]) -> int:
|
|
116
|
+
"""删除SQLite数据"""
|
|
117
|
+
query, params = self._build_delete_query(table, condition)
|
|
118
|
+
result = self.execute(query, params)
|
|
119
|
+
logger.debug(f"删除了 {result} 条记录")
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def select(self, table: str,
|
|
123
|
+
fields: Optional[List[str]] = None,
|
|
124
|
+
condition: Optional[Dict[str, Any]] = None,
|
|
125
|
+
limit: Optional[int] = None,
|
|
126
|
+
offset: Optional[int] = None,
|
|
127
|
+
order_by: Optional[List[tuple]] = None) -> List[Dict]:
|
|
128
|
+
"""查询SQLite数据"""
|
|
129
|
+
query, params = self._build_select_query(
|
|
130
|
+
table, fields, condition, limit, offset, order_by
|
|
131
|
+
)
|
|
132
|
+
result = self.execute(query, params)
|
|
133
|
+
logger.debug(f"查询到 {len(result)} 条记录")
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def execute_script(self, script: str) -> None:
|
|
137
|
+
"""
|
|
138
|
+
执行SQL脚本
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
script: SQL脚本内容
|
|
142
|
+
"""
|
|
143
|
+
if not self.is_connected():
|
|
144
|
+
raise ConnectionError("数据库未连接")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
self.connection.executescript(script)
|
|
148
|
+
self.connection.commit()
|
|
149
|
+
logger.debug("SQL脚本执行成功")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"SQL脚本执行失败: {e}")
|
|
152
|
+
raise QueryError(f"SQL脚本执行失败: {e}")
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase数据库客户端
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from ..core.base import BaseClient
|
|
9
|
+
from ..exceptions import ConnectionError, QueryError, NotSupportedError
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SupabaseClient(BaseClient):
|
|
15
|
+
"""Supabase数据库客户端实现"""
|
|
16
|
+
|
|
17
|
+
def _validate_config(self) -> None:
|
|
18
|
+
"""验证Supabase配置"""
|
|
19
|
+
super()._validate_config()
|
|
20
|
+
required = ['url', 'key']
|
|
21
|
+
missing = [key for key in required if key not in self.config]
|
|
22
|
+
if missing:
|
|
23
|
+
raise ValueError(f"Supabase配置缺少必需字段: {', '.join(missing)}")
|
|
24
|
+
|
|
25
|
+
def connect(self) -> bool:
|
|
26
|
+
"""连接Supabase"""
|
|
27
|
+
try:
|
|
28
|
+
from supabase import create_client, Client
|
|
29
|
+
|
|
30
|
+
url = self.config['url']
|
|
31
|
+
key = self.config['key']
|
|
32
|
+
|
|
33
|
+
self.connection: Client = create_client(url, key)
|
|
34
|
+
self._connected = True
|
|
35
|
+
logger.info(f"Supabase连接成功: {url}")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise ConnectionError("未安装supabase库,请运行: pip install supabase")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
self._connected = False
|
|
42
|
+
logger.error(f"Supabase连接失败: {e}")
|
|
43
|
+
raise ConnectionError(f"Supabase连接失败: {e}")
|
|
44
|
+
|
|
45
|
+
def disconnect(self) -> bool:
|
|
46
|
+
"""断开Supabase连接"""
|
|
47
|
+
# Supabase客户端不需要显式断开连接
|
|
48
|
+
self._connected = False
|
|
49
|
+
logger.info("Supabase会话结束")
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def is_connected(self) -> bool:
|
|
53
|
+
"""检查Supabase连接状态"""
|
|
54
|
+
# Supabase使用REST API,只要有客户端实例就认为是连接的
|
|
55
|
+
return self.connection is not None and self._connected
|
|
56
|
+
|
|
57
|
+
def execute(self, query: str, params: Optional[tuple] = None) -> Any:
|
|
58
|
+
"""
|
|
59
|
+
Supabase不直接支持SQL查询
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
NotSupportedError: 总是抛出此异常
|
|
63
|
+
"""
|
|
64
|
+
raise NotSupportedError("Supabase使用REST API,请使用insert/update/delete/select方法")
|
|
65
|
+
|
|
66
|
+
def insert(self, table: str, data: Dict[str, Any]) -> Any:
|
|
67
|
+
"""插入数据到Supabase"""
|
|
68
|
+
if not self.is_connected():
|
|
69
|
+
raise ConnectionError("数据库未连接")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
result = self.connection.table(table).insert(data).execute()
|
|
73
|
+
logger.debug(f"插入数据成功: {result.data}")
|
|
74
|
+
return result.data
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"插入数据失败: {e}")
|
|
78
|
+
raise QueryError(f"插入数据失败: {e}")
|
|
79
|
+
|
|
80
|
+
def insert_many(self, table: str, data_list: List[Dict[str, Any]]) -> List[Dict]:
|
|
81
|
+
"""
|
|
82
|
+
批量插入数据
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
table: 表名
|
|
86
|
+
data_list: 数据列表
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List[Dict]: 插入的数据
|
|
90
|
+
"""
|
|
91
|
+
if not self.is_connected():
|
|
92
|
+
raise ConnectionError("数据库未连接")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
result = self.connection.table(table).insert(data_list).execute()
|
|
96
|
+
logger.debug(f"批量插入 {len(result.data)} 条数据")
|
|
97
|
+
return result.data
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"批量插入数据失败: {e}")
|
|
101
|
+
raise QueryError(f"批量插入数据失败: {e}")
|
|
102
|
+
|
|
103
|
+
def update(self, table: str, data: Dict[str, Any],
|
|
104
|
+
condition: Dict[str, Any]) -> int:
|
|
105
|
+
"""更新Supabase数据"""
|
|
106
|
+
if not self.is_connected():
|
|
107
|
+
raise ConnectionError("数据库未连接")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
query = self.connection.table(table).update(data)
|
|
111
|
+
|
|
112
|
+
# 添加条件
|
|
113
|
+
for key, value in condition.items():
|
|
114
|
+
query = query.eq(key, value)
|
|
115
|
+
|
|
116
|
+
result = query.execute()
|
|
117
|
+
count = len(result.data)
|
|
118
|
+
logger.debug(f"更新了 {count} 条记录")
|
|
119
|
+
return count
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"更新数据失败: {e}")
|
|
123
|
+
raise QueryError(f"更新数据失败: {e}")
|
|
124
|
+
|
|
125
|
+
def delete(self, table: str, condition: Dict[str, Any]) -> int:
|
|
126
|
+
"""删除Supabase数据"""
|
|
127
|
+
if not self.is_connected():
|
|
128
|
+
raise ConnectionError("数据库未连接")
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
query = self.connection.table(table).delete()
|
|
132
|
+
|
|
133
|
+
# 添加条件
|
|
134
|
+
for key, value in condition.items():
|
|
135
|
+
query = query.eq(key, value)
|
|
136
|
+
|
|
137
|
+
result = query.execute()
|
|
138
|
+
count = len(result.data)
|
|
139
|
+
logger.debug(f"删除了 {count} 条记录")
|
|
140
|
+
return count
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"删除数据失败: {e}")
|
|
144
|
+
raise QueryError(f"删除数据失败: {e}")
|
|
145
|
+
|
|
146
|
+
def select(self, table: str,
|
|
147
|
+
fields: Optional[List[str]] = None,
|
|
148
|
+
condition: Optional[Dict[str, Any]] = None,
|
|
149
|
+
limit: Optional[int] = None,
|
|
150
|
+
offset: Optional[int] = None,
|
|
151
|
+
order_by: Optional[List[tuple]] = None) -> List[Dict]:
|
|
152
|
+
"""查询Supabase数据"""
|
|
153
|
+
if not self.is_connected():
|
|
154
|
+
raise ConnectionError("数据库未连接")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# 构建字段选择
|
|
158
|
+
fields_str = ','.join(fields) if fields else '*'
|
|
159
|
+
query = self.connection.table(table).select(fields_str)
|
|
160
|
+
|
|
161
|
+
# 添加条件
|
|
162
|
+
if condition:
|
|
163
|
+
for key, value in condition.items():
|
|
164
|
+
query = query.eq(key, value)
|
|
165
|
+
|
|
166
|
+
# 排序
|
|
167
|
+
if order_by:
|
|
168
|
+
for field, direction in order_by:
|
|
169
|
+
desc = direction.upper() == 'DESC'
|
|
170
|
+
query = query.order(field, desc=desc)
|
|
171
|
+
|
|
172
|
+
# 分页
|
|
173
|
+
if offset is not None:
|
|
174
|
+
query = query.range(offset, offset + (limit or 999999))
|
|
175
|
+
elif limit is not None:
|
|
176
|
+
query = query.limit(limit)
|
|
177
|
+
|
|
178
|
+
result = query.execute()
|
|
179
|
+
logger.debug(f"查询到 {len(result.data)} 条记录")
|
|
180
|
+
return result.data
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"查询数据失败: {e}")
|
|
184
|
+
raise QueryError(f"查询数据失败: {e}")
|
|
185
|
+
|
|
186
|
+
def upsert(self, table: str, data: Union[Dict[str, Any], List[Dict[str, Any]]]) -> Any:
|
|
187
|
+
"""
|
|
188
|
+
插入或更新数据(如果存在则更新)
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
table: 表名
|
|
192
|
+
data: 单条或多条数据
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
插入或更新的数据
|
|
196
|
+
"""
|
|
197
|
+
if not self.is_connected():
|
|
198
|
+
raise ConnectionError("数据库未连接")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
result = self.connection.table(table).upsert(data).execute()
|
|
202
|
+
logger.debug(f"Upsert操作成功: {result.data}")
|
|
203
|
+
return result.data
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"Upsert操作失败: {e}")
|
|
207
|
+
raise QueryError(f"Upsert操作失败: {e}")
|
|
208
|
+
|
|
209
|
+
def rpc(self, function_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
210
|
+
"""
|
|
211
|
+
调用Supabase存储过程
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
function_name: 函数名
|
|
215
|
+
params: 参数
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
函数返回结果
|
|
219
|
+
"""
|
|
220
|
+
if not self.is_connected():
|
|
221
|
+
raise ConnectionError("数据库未连接")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
result = self.connection.rpc(function_name, params or {}).execute()
|
|
225
|
+
logger.debug(f"调用RPC函数成功: {function_name}")
|
|
226
|
+
return result.data
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"调用RPC函数失败: {e}")
|
|
230
|
+
raise QueryError(f"调用RPC函数失败: {e}")
|