memorisdk 1.0.2__py3-none-any.whl → 2.0.0__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.

Potentially problematic release.


This version of memorisdk might be problematic. Click here for more details.

Files changed (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1638 -531
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/METADATA +44 -17
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.2.dist-info/RECORD +0 -44
  43. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,283 @@
1
+ """
2
+ Base database connector interface for Memori
3
+ Provides abstraction layer for different database backends
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+
11
+ class DatabaseType(str, Enum):
12
+ """Supported database types"""
13
+
14
+ SQLITE = "sqlite"
15
+ MYSQL = "mysql"
16
+ POSTGRESQL = "postgresql"
17
+
18
+
19
+ class SearchStrategy(str, Enum):
20
+ """Available search strategies"""
21
+
22
+ NATIVE = "native" # Use database-specific full-text search
23
+ LIKE_FALLBACK = "like" # Use LIKE queries with indexing
24
+ HYBRID = "hybrid" # Native with fallback to LIKE
25
+ EXTERNAL = "external" # External search engine (future)
26
+
27
+
28
+ class BaseDatabaseConnector(ABC):
29
+ """Abstract base class for database connectors"""
30
+
31
+ def __init__(self, connection_config: Dict[str, Any]):
32
+ self.connection_config = connection_config
33
+ self.database_type = self._detect_database_type()
34
+
35
+ @abstractmethod
36
+ def _detect_database_type(self) -> DatabaseType:
37
+ """Detect the database type from connection config"""
38
+ pass
39
+
40
+ @abstractmethod
41
+ def get_connection(self):
42
+ """Get database connection"""
43
+ pass
44
+
45
+ @abstractmethod
46
+ def execute_query(
47
+ self, query: str, params: Optional[List[Any]] = None
48
+ ) -> List[Dict[str, Any]]:
49
+ """Execute a query and return results"""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def execute_insert(self, query: str, params: Optional[List[Any]] = None) -> str:
54
+ """Execute an insert query and return the inserted row ID"""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def execute_update(self, query: str, params: Optional[List[Any]] = None) -> int:
59
+ """Execute an update query and return number of affected rows"""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def execute_delete(self, query: str, params: Optional[List[Any]] = None) -> int:
64
+ """Execute a delete query and return number of affected rows"""
65
+ pass
66
+
67
+ @abstractmethod
68
+ def execute_transaction(
69
+ self, queries: List[Tuple[str, Optional[List[Any]]]]
70
+ ) -> bool:
71
+ """Execute multiple queries in a transaction"""
72
+ pass
73
+
74
+ @abstractmethod
75
+ def test_connection(self) -> bool:
76
+ """Test if the database connection is working"""
77
+ pass
78
+
79
+ @abstractmethod
80
+ def initialize_schema(self, schema_sql: Optional[str] = None):
81
+ """Initialize database schema"""
82
+ pass
83
+
84
+ @abstractmethod
85
+ def supports_full_text_search(self) -> bool:
86
+ """Check if the database supports native full-text search"""
87
+ pass
88
+
89
+ @abstractmethod
90
+ def create_full_text_index(
91
+ self, table: str, columns: List[str], index_name: str
92
+ ) -> str:
93
+ """Create database-specific full-text search index"""
94
+ pass
95
+
96
+ @abstractmethod
97
+ def get_database_info(self) -> Dict[str, Any]:
98
+ """Get database information and capabilities"""
99
+ pass
100
+
101
+
102
+ class BaseSearchAdapter(ABC):
103
+ """Abstract base class for search adapters"""
104
+
105
+ def __init__(self, connector: BaseDatabaseConnector):
106
+ self.connector = connector
107
+ self.database_type = connector.database_type
108
+
109
+ @abstractmethod
110
+ def execute_fulltext_search(
111
+ self,
112
+ query: str,
113
+ namespace: str = "default",
114
+ category_filter: Optional[List[str]] = None,
115
+ limit: int = 10,
116
+ ) -> List[Dict[str, Any]]:
117
+ """Execute full-text search using database-specific methods"""
118
+ pass
119
+
120
+ @abstractmethod
121
+ def create_search_indexes(self) -> List[str]:
122
+ """Create search indexes for optimal performance"""
123
+ pass
124
+
125
+ @abstractmethod
126
+ def translate_search_query(self, query: str) -> str:
127
+ """Translate search query to database-specific syntax"""
128
+ pass
129
+
130
+ def execute_fallback_search(
131
+ self,
132
+ query: str,
133
+ namespace: str = "default",
134
+ category_filter: Optional[List[str]] = None,
135
+ limit: int = 10,
136
+ ) -> List[Dict[str, Any]]:
137
+ """Execute fallback LIKE-based search with proper parameterization"""
138
+ try:
139
+ # Input validation and sanitization
140
+ sanitized_query = str(query).strip() if query else ""
141
+ sanitized_namespace = str(namespace).strip()
142
+ sanitized_limit = max(1, min(int(limit), 1000)) # Limit between 1 and 1000
143
+
144
+ results = []
145
+
146
+ # Validate and sanitize category filter
147
+ sanitized_categories = []
148
+ if category_filter and isinstance(category_filter, list):
149
+ sanitized_categories = [
150
+ str(cat).strip() for cat in category_filter if cat
151
+ ]
152
+
153
+ # Search short-term memory with parameterized query
154
+ if sanitized_categories:
155
+ category_placeholders = ",".join(["?"] * len(sanitized_categories))
156
+ short_term_query = f"""
157
+ SELECT *, 'short_term' as memory_type, 'like_fallback' as search_strategy
158
+ FROM short_term_memory
159
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
160
+ AND category_primary IN ({category_placeholders})
161
+ ORDER BY importance_score DESC, created_at DESC
162
+ LIMIT ?
163
+ """
164
+ short_term_params = (
165
+ [
166
+ sanitized_namespace,
167
+ f"%{sanitized_query}%",
168
+ f"%{sanitized_query}%",
169
+ ]
170
+ + sanitized_categories
171
+ + [sanitized_limit]
172
+ )
173
+ else:
174
+ short_term_query = """
175
+ SELECT *, 'short_term' as memory_type, 'like_fallback' as search_strategy
176
+ FROM short_term_memory
177
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
178
+ ORDER BY importance_score DESC, created_at DESC
179
+ LIMIT ?
180
+ """
181
+ short_term_params = [
182
+ sanitized_namespace,
183
+ f"%{sanitized_query}%",
184
+ f"%{sanitized_query}%",
185
+ sanitized_limit,
186
+ ]
187
+
188
+ try:
189
+ results.extend(
190
+ self.connector.execute_query(short_term_query, short_term_params)
191
+ )
192
+ except Exception:
193
+ # Log error but continue with long-term search
194
+ pass
195
+
196
+ # Search long-term memory with parameterized query
197
+ if sanitized_categories:
198
+ category_placeholders = ",".join(["?"] * len(sanitized_categories))
199
+ long_term_query = f"""
200
+ SELECT *, 'long_term' as memory_type, 'like_fallback' as search_strategy
201
+ FROM long_term_memory
202
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
203
+ AND category_primary IN ({category_placeholders})
204
+ ORDER BY importance_score DESC, created_at DESC
205
+ LIMIT ?
206
+ """
207
+ long_term_params = (
208
+ [
209
+ sanitized_namespace,
210
+ f"%{sanitized_query}%",
211
+ f"%{sanitized_query}%",
212
+ ]
213
+ + sanitized_categories
214
+ + [sanitized_limit]
215
+ )
216
+ else:
217
+ long_term_query = """
218
+ SELECT *, 'long_term' as memory_type, 'like_fallback' as search_strategy
219
+ FROM long_term_memory
220
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
221
+ ORDER BY importance_score DESC, created_at DESC
222
+ LIMIT ?
223
+ """
224
+ long_term_params = [
225
+ sanitized_namespace,
226
+ f"%{sanitized_query}%",
227
+ f"%{sanitized_query}%",
228
+ sanitized_limit,
229
+ ]
230
+
231
+ try:
232
+ results.extend(
233
+ self.connector.execute_query(long_term_query, long_term_params)
234
+ )
235
+ except Exception:
236
+ # Log error but continue
237
+ pass
238
+
239
+ return results[:sanitized_limit] # Ensure final limit
240
+
241
+ except Exception:
242
+ # Return empty results on error instead of raising exception
243
+ return []
244
+
245
+
246
+ class BaseSchemaGenerator(ABC):
247
+ """Abstract base class for database-specific schema generators"""
248
+
249
+ def __init__(self, database_type: DatabaseType):
250
+ self.database_type = database_type
251
+
252
+ @abstractmethod
253
+ def generate_core_schema(self) -> str:
254
+ """Generate core tables schema"""
255
+ pass
256
+
257
+ @abstractmethod
258
+ def generate_indexes(self) -> str:
259
+ """Generate database-specific indexes"""
260
+ pass
261
+
262
+ @abstractmethod
263
+ def generate_search_setup(self) -> str:
264
+ """Generate search-related schema (indexes, triggers, etc.)"""
265
+ pass
266
+
267
+ @abstractmethod
268
+ def get_data_type_mappings(self) -> Dict[str, str]:
269
+ """Get database-specific data type mappings"""
270
+ pass
271
+
272
+ def generate_full_schema(self) -> str:
273
+ """Generate complete schema"""
274
+ schema_parts = [
275
+ "-- Generated schema for " + self.database_type.value.upper(),
276
+ "",
277
+ self.generate_core_schema(),
278
+ "",
279
+ self.generate_indexes(),
280
+ "",
281
+ self.generate_search_setup(),
282
+ ]
283
+ return "\n".join(schema_parts)
@@ -1,22 +1,24 @@
1
1
  """
2
- MySQL connector for Memori v1.0
2
+ MySQL connector for Memori v2.0
3
+ Implements BaseDatabaseConnector interface with FULLTEXT search support
3
4
  """
4
5
 
5
- from typing import Any, Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+ from urllib.parse import urlparse
6
8
 
7
9
  from loguru import logger
8
10
 
9
11
  from ...utils.exceptions import DatabaseError
12
+ from .base_connector import BaseDatabaseConnector, DatabaseType
10
13
 
11
14
 
12
- class MySQLConnector:
13
- """MySQL database connector"""
15
+ class MySQLConnector(BaseDatabaseConnector):
16
+ """MySQL database connector with FULLTEXT search support"""
14
17
 
15
- def __init__(self, connection_config: Dict[str, str]):
16
- """Initialize MySQL connector"""
17
- self.connection_config = connection_config
18
+ def __init__(self, connection_config: Dict[str, Any]):
18
19
  self._mysql = None
19
20
  self._setup_mysql()
21
+ super().__init__(connection_config)
20
22
 
21
23
  def _setup_mysql(self):
22
24
  """Setup MySQL connection library"""
@@ -30,17 +32,67 @@ class MySQLConnector:
30
32
  "Install it with: pip install mysql-connector-python"
31
33
  )
32
34
 
35
+ def _detect_database_type(self) -> DatabaseType:
36
+ """Detect database type from connection config"""
37
+ return DatabaseType.MYSQL
38
+
39
+ def _parse_connection_string(self, connection_string: str) -> Dict[str, Any]:
40
+ """Parse MySQL connection string into connection config"""
41
+ if connection_string.startswith("mysql://"):
42
+ parsed = urlparse(connection_string)
43
+ return {
44
+ "host": parsed.hostname or "localhost",
45
+ "port": parsed.port or 3306,
46
+ "user": parsed.username,
47
+ "password": parsed.password,
48
+ "database": parsed.path.lstrip("/") if parsed.path else None,
49
+ }
50
+ elif isinstance(self.connection_config, dict):
51
+ return self.connection_config
52
+ else:
53
+ raise DatabaseError(
54
+ f"Invalid MySQL connection configuration: {connection_string}"
55
+ )
56
+
33
57
  def get_connection(self):
34
- """Get MySQL connection"""
58
+ """Get MySQL connection with proper configuration"""
35
59
  try:
36
- conn = self._mysql.connect(**self.connection_config)
60
+ # Parse connection string if needed
61
+ if isinstance(self.connection_config, str):
62
+ config = self._parse_connection_string(self.connection_config)
63
+ else:
64
+ config = self.connection_config.copy()
65
+
66
+ # Set MySQL-specific options
67
+ config.update(
68
+ {
69
+ "charset": "utf8mb4",
70
+ "collation": "utf8mb4_unicode_ci",
71
+ "autocommit": False,
72
+ "use_pure": True, # Use pure Python implementation
73
+ }
74
+ )
75
+
76
+ conn = self._mysql.connect(**config)
77
+
78
+ # Set session variables for optimal performance
79
+ cursor = conn.cursor()
80
+ cursor.execute(
81
+ "SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'"
82
+ )
83
+ cursor.execute("SET SESSION innodb_lock_wait_timeout = 30")
84
+ cursor.execute(
85
+ "SET SESSION ft_min_word_len = 3"
86
+ ) # FULLTEXT minimum word length
87
+ cursor.close()
88
+
37
89
  return conn
38
90
 
39
91
  except Exception as e:
40
92
  raise DatabaseError(f"Failed to connect to MySQL database: {e}")
41
93
 
42
94
  def execute_query(
43
- self, query: str, params: Optional[List] = None
95
+ self, query: str, params: Optional[List[Any]] = None
44
96
  ) -> List[Dict[str, Any]]:
45
97
  """Execute a query and return results"""
46
98
  try:
@@ -60,7 +112,7 @@ class MySQLConnector:
60
112
  except Exception as e:
61
113
  raise DatabaseError(f"Failed to execute query: {e}")
62
114
 
63
- def execute_insert(self, query: str, params: Optional[List] = None) -> str:
115
+ def execute_insert(self, query: str, params: Optional[List[Any]] = None) -> str:
64
116
  """Execute an insert query and return the inserted row ID"""
65
117
  try:
66
118
  with self.get_connection() as conn:
@@ -80,7 +132,7 @@ class MySQLConnector:
80
132
  except Exception as e:
81
133
  raise DatabaseError(f"Failed to execute insert: {e}")
82
134
 
83
- def execute_update(self, query: str, params: Optional[List] = None) -> int:
135
+ def execute_update(self, query: str, params: Optional[List[Any]] = None) -> int:
84
136
  """Execute an update query and return number of affected rows"""
85
137
  try:
86
138
  with self.get_connection() as conn:
@@ -100,7 +152,7 @@ class MySQLConnector:
100
152
  except Exception as e:
101
153
  raise DatabaseError(f"Failed to execute update: {e}")
102
154
 
103
- def execute_delete(self, query: str, params: Optional[List] = None) -> int:
155
+ def execute_delete(self, query: str, params: Optional[List[Any]] = None) -> int:
104
156
  """Execute a delete query and return number of affected rows"""
105
157
  try:
106
158
  with self.get_connection() as conn:
@@ -120,13 +172,17 @@ class MySQLConnector:
120
172
  except Exception as e:
121
173
  raise DatabaseError(f"Failed to execute delete: {e}")
122
174
 
123
- def execute_transaction(self, queries: List[tuple]) -> bool:
175
+ def execute_transaction(
176
+ self, queries: List[Tuple[str, Optional[List[Any]]]]
177
+ ) -> bool:
124
178
  """Execute multiple queries in a transaction"""
125
179
  try:
126
180
  with self.get_connection() as conn:
127
181
  cursor = conn.cursor()
128
182
 
129
183
  try:
184
+ conn.start_transaction()
185
+
130
186
  for query, params in queries:
131
187
  if params:
132
188
  cursor.execute(query, params)
@@ -143,7 +199,7 @@ class MySQLConnector:
143
199
  raise e
144
200
 
145
201
  except Exception as e:
146
- logger.error(f"Transaction failed: {e}")
202
+ logger.error(f"MySQL transaction failed: {e}")
147
203
  return False
148
204
 
149
205
  def test_connection(self) -> bool:
@@ -151,9 +207,175 @@ class MySQLConnector:
151
207
  try:
152
208
  with self.get_connection() as conn:
153
209
  cursor = conn.cursor()
154
- cursor.execute("SELECT 1")
210
+ cursor.execute("SELECT 1 as test")
211
+ result = cursor.fetchone()
212
+ cursor.close()
213
+ return result is not None
214
+ except Exception as e:
215
+ logger.error(f"MySQL connection test failed: {e}")
216
+ return False
217
+
218
+ def initialize_schema(self, schema_sql: Optional[str] = None):
219
+ """Initialize database schema"""
220
+ try:
221
+ if not schema_sql:
222
+ # Use MySQL-specific schema
223
+ from ..schema_generators.mysql_schema_generator import (
224
+ MySQLSchemaGenerator,
225
+ )
226
+
227
+ schema_generator = MySQLSchemaGenerator()
228
+ schema_sql = schema_generator.generate_full_schema()
229
+
230
+ # Execute schema using transaction
231
+ with self.get_connection() as conn:
232
+ cursor = conn.cursor()
233
+
234
+ try:
235
+ conn.start_transaction()
236
+
237
+ # Split schema into individual statements
238
+ statements = self._split_mysql_statements(schema_sql)
239
+
240
+ for statement in statements:
241
+ statement = statement.strip()
242
+ if statement and not statement.startswith("--"):
243
+ cursor.execute(statement)
244
+
245
+ conn.commit()
246
+ logger.info("MySQL database schema initialized successfully")
247
+
248
+ except Exception as e:
249
+ conn.rollback()
250
+ logger.error(f"Failed to initialize MySQL schema: {e}")
251
+ raise DatabaseError(f"Schema initialization failed: {e}")
252
+ finally:
253
+ cursor.close()
254
+
255
+ except Exception as e:
256
+ logger.error(f"Failed to initialize MySQL schema: {e}")
257
+ raise DatabaseError(f"Failed to initialize MySQL schema: {e}")
258
+
259
+ def _split_mysql_statements(self, schema_sql: str) -> List[str]:
260
+ """Split SQL schema into individual statements handling MySQL syntax"""
261
+ statements = []
262
+ current_statement = []
263
+
264
+ for line in schema_sql.split("\n"):
265
+ line = line.strip()
266
+
267
+ # Skip comments and empty lines
268
+ if not line or line.startswith("--"):
269
+ continue
270
+
271
+ current_statement.append(line)
272
+
273
+ # MySQL uses semicolon to end statements
274
+ if line.endswith(";"):
275
+ statement = " ".join(current_statement)
276
+ if statement.strip():
277
+ statements.append(statement)
278
+ current_statement = []
279
+
280
+ # Add any remaining statement
281
+ if current_statement:
282
+ statement = " ".join(current_statement)
283
+ if statement.strip():
284
+ statements.append(statement)
285
+
286
+ return statements
287
+
288
+ def supports_full_text_search(self) -> bool:
289
+ """Check if MySQL supports FULLTEXT search"""
290
+ try:
291
+ with self.get_connection() as conn:
292
+ cursor = conn.cursor()
293
+
294
+ # Check MySQL version and InnoDB support for FULLTEXT
295
+ cursor.execute("SELECT VERSION() as version")
296
+ version_result = cursor.fetchone()
297
+
298
+ cursor.execute("SHOW ENGINES")
299
+ engines = cursor.fetchall()
300
+
155
301
  cursor.close()
156
- return True
302
+
303
+ # MySQL 5.6+ with InnoDB supports FULLTEXT
304
+ version = version_result["version"] if version_result else "0.0.0"
305
+ version_parts = [int(x.split("-")[0]) for x in version.split(".")[:2]]
306
+
307
+ innodb_available = any(
308
+ engine["Engine"] == "InnoDB"
309
+ and engine["Support"] in ("YES", "DEFAULT")
310
+ for engine in engines
311
+ )
312
+
313
+ return (
314
+ version_parts[0] > 5
315
+ or (version_parts[0] == 5 and version_parts[1] >= 6)
316
+ ) and innodb_available
317
+
157
318
  except Exception as e:
158
- logger.error(f"Connection test failed: {e}")
319
+ logger.warning(f"Could not determine MySQL FULLTEXT support: {e}")
159
320
  return False
321
+
322
+ def create_full_text_index(
323
+ self, table: str, columns: List[str], index_name: str
324
+ ) -> str:
325
+ """Create MySQL FULLTEXT index"""
326
+ columns_str = ", ".join(columns)
327
+ return f"ALTER TABLE {table} ADD FULLTEXT INDEX {index_name} ({columns_str})"
328
+
329
+ def get_database_info(self) -> Dict[str, Any]:
330
+ """Get MySQL database information and capabilities"""
331
+ try:
332
+ with self.get_connection() as conn:
333
+ cursor = conn.cursor(dictionary=True)
334
+
335
+ info = {}
336
+
337
+ # Basic version info
338
+ cursor.execute("SELECT VERSION() as version")
339
+ version_result = cursor.fetchone()
340
+ info["version"] = (
341
+ version_result["version"] if version_result else "unknown"
342
+ )
343
+
344
+ # Storage engines
345
+ cursor.execute("SHOW ENGINES")
346
+ info["engines"] = cursor.fetchall()
347
+
348
+ # Database name
349
+ cursor.execute("SELECT DATABASE() as db_name")
350
+ db_result = cursor.fetchone()
351
+ info["database"] = db_result["db_name"] if db_result else "unknown"
352
+
353
+ # Character set info
354
+ cursor.execute("SHOW VARIABLES LIKE 'character_set_%'")
355
+ charset_vars = cursor.fetchall()
356
+ info["character_sets"] = {
357
+ var["Variable_name"]: var["Value"] for var in charset_vars
358
+ }
359
+
360
+ # FULLTEXT configuration
361
+ cursor.execute("SHOW VARIABLES LIKE 'ft_%'")
362
+ fulltext_vars = cursor.fetchall()
363
+ info["fulltext_config"] = {
364
+ var["Variable_name"]: var["Value"] for var in fulltext_vars
365
+ }
366
+
367
+ # Connection info
368
+ info["database_type"] = self.database_type.value
369
+ info["fulltext_support"] = self.supports_full_text_search()
370
+
371
+ cursor.close()
372
+ return info
373
+
374
+ except Exception as e:
375
+ logger.warning(f"Could not get MySQL database info: {e}")
376
+ return {
377
+ "database_type": self.database_type.value,
378
+ "version": "unknown",
379
+ "fulltext_support": False,
380
+ "error": str(e),
381
+ }