memorisdk 1.0.2__py3-none-any.whl → 2.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.
Potentially problematic release.
This version of memorisdk might be problematic. Click here for more details.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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"
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|