memorisdk 2.0.1__py3-none-any.whl → 2.1.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 +3 -3
- memori/agents/conscious_agent.py +289 -77
- memori/agents/memory_agent.py +19 -9
- memori/agents/retrieval_agent.py +59 -51
- memori/config/manager.py +7 -7
- memori/config/memory_manager.py +25 -25
- memori/config/settings.py +13 -6
- memori/core/conversation.py +15 -15
- memori/core/database.py +14 -13
- memori/core/memory.py +376 -105
- memori/core/providers.py +25 -25
- memori/database/__init__.py +11 -0
- memori/database/adapters/__init__.py +11 -0
- memori/database/adapters/mongodb_adapter.py +739 -0
- memori/database/adapters/mysql_adapter.py +8 -8
- memori/database/adapters/postgresql_adapter.py +6 -6
- memori/database/adapters/sqlite_adapter.py +6 -6
- memori/database/auto_creator.py +8 -9
- memori/database/connection_utils.py +5 -5
- memori/database/connectors/__init__.py +11 -0
- memori/database/connectors/base_connector.py +18 -19
- memori/database/connectors/mongodb_connector.py +654 -0
- memori/database/connectors/mysql_connector.py +13 -15
- memori/database/connectors/postgres_connector.py +12 -12
- memori/database/connectors/sqlite_connector.py +11 -11
- memori/database/models.py +2 -2
- memori/database/mongodb_manager.py +1484 -0
- memori/database/queries/base_queries.py +3 -4
- memori/database/queries/chat_queries.py +3 -5
- memori/database/queries/entity_queries.py +3 -5
- memori/database/queries/memory_queries.py +3 -5
- memori/database/query_translator.py +11 -11
- memori/database/schema_generators/__init__.py +11 -0
- memori/database/schema_generators/mongodb_schema_generator.py +666 -0
- memori/database/schema_generators/mysql_schema_generator.py +2 -4
- memori/database/search/__init__.py +11 -0
- memori/database/search/mongodb_search_adapter.py +653 -0
- memori/database/search/mysql_search_adapter.py +8 -8
- memori/database/search/sqlite_search_adapter.py +6 -6
- memori/database/search_service.py +17 -17
- memori/database/sqlalchemy_manager.py +10 -12
- memori/integrations/__init__.py +1 -1
- memori/integrations/anthropic_integration.py +1 -3
- memori/integrations/litellm_integration.py +23 -6
- memori/integrations/openai_integration.py +31 -3
- memori/tools/memory_tool.py +10 -9
- memori/utils/exceptions.py +58 -58
- memori/utils/helpers.py +11 -12
- memori/utils/input_validator.py +10 -12
- memori/utils/logging.py +4 -4
- memori/utils/pydantic_models.py +57 -57
- memori/utils/query_builder.py +20 -20
- memori/utils/security_audit.py +28 -28
- memori/utils/security_integration.py +9 -9
- memori/utils/transaction_manager.py +20 -19
- memori/utils/validators.py +6 -6
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/METADATA +23 -12
- memorisdk-2.1.1.dist-info/RECORD +71 -0
- memorisdk-2.0.1.dist-info/RECORD +0 -66
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/WHEEL +0 -0
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB connector for Memori
|
|
3
|
+
Provides MongoDB-specific implementation of the database connector interface
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pymongo import MongoClient
|
|
16
|
+
from pymongo.collection import Collection
|
|
17
|
+
from pymongo.database import Database
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import pymongo # noqa: F401
|
|
21
|
+
from pymongo import MongoClient as _MongoClient
|
|
22
|
+
from pymongo.collection import Collection as _Collection
|
|
23
|
+
from pymongo.database import Database as _Database
|
|
24
|
+
from pymongo.errors import ConnectionFailure, OperationFailure # noqa: F401
|
|
25
|
+
|
|
26
|
+
PYMONGO_AVAILABLE = True
|
|
27
|
+
MongoClient = _MongoClient # type: ignore
|
|
28
|
+
Collection = _Collection # type: ignore
|
|
29
|
+
Database = _Database # type: ignore
|
|
30
|
+
except ImportError:
|
|
31
|
+
PYMONGO_AVAILABLE = False
|
|
32
|
+
MongoClient = None # type: ignore
|
|
33
|
+
Collection = None # type: ignore
|
|
34
|
+
Database = None # type: ignore
|
|
35
|
+
|
|
36
|
+
from ...utils.exceptions import DatabaseError
|
|
37
|
+
from .base_connector import BaseDatabaseConnector, DatabaseType
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MongoDBConnector(BaseDatabaseConnector):
|
|
41
|
+
"""MongoDB database connector with Atlas Vector Search support"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, connection_config):
|
|
44
|
+
"""Initialize MongoDB connector"""
|
|
45
|
+
if not PYMONGO_AVAILABLE:
|
|
46
|
+
raise DatabaseError(
|
|
47
|
+
"pymongo is required for MongoDB support. Install with: pip install pymongo"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if isinstance(connection_config, str):
|
|
51
|
+
self.connection_string = connection_config
|
|
52
|
+
self.connection_config = {"connection_string": connection_config}
|
|
53
|
+
else:
|
|
54
|
+
self.connection_string = connection_config.get(
|
|
55
|
+
"connection_string", "mongodb://localhost:27017"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Parse MongoDB connection string
|
|
59
|
+
self._parse_connection_string()
|
|
60
|
+
|
|
61
|
+
# MongoDB-specific settings
|
|
62
|
+
self.client = None
|
|
63
|
+
self.database = None
|
|
64
|
+
self._collections = {}
|
|
65
|
+
|
|
66
|
+
super().__init__(connection_config)
|
|
67
|
+
|
|
68
|
+
def _detect_database_type(self) -> DatabaseType:
|
|
69
|
+
"""Detect database type from connection config"""
|
|
70
|
+
return DatabaseType.MONGODB
|
|
71
|
+
|
|
72
|
+
def _parse_connection_string(self):
|
|
73
|
+
"""Parse MongoDB connection string to extract components"""
|
|
74
|
+
try:
|
|
75
|
+
# Handle MongoDB connection strings properly (including replica sets)
|
|
76
|
+
if self.connection_string.startswith(
|
|
77
|
+
"mongodb://"
|
|
78
|
+
) or self.connection_string.startswith("mongodb+srv://"):
|
|
79
|
+
# For MongoDB URIs, extract database name and basic info for logging
|
|
80
|
+
# but let pymongo handle the full parsing
|
|
81
|
+
if "?" in self.connection_string:
|
|
82
|
+
uri_part, query_part = self.connection_string.split("?", 1)
|
|
83
|
+
else:
|
|
84
|
+
uri_part, query_part = self.connection_string, ""
|
|
85
|
+
|
|
86
|
+
# Extract database name from path
|
|
87
|
+
if "/" in uri_part:
|
|
88
|
+
path_part = uri_part.split("/")[-1]
|
|
89
|
+
self.database_name = path_part if path_part else "memori"
|
|
90
|
+
else:
|
|
91
|
+
self.database_name = "memori"
|
|
92
|
+
|
|
93
|
+
# Extract host info for logging
|
|
94
|
+
is_srv_uri = self.connection_string.startswith("mongodb+srv://")
|
|
95
|
+
|
|
96
|
+
if is_srv_uri:
|
|
97
|
+
# For SRV URIs, extract the service hostname
|
|
98
|
+
if "@" in uri_part:
|
|
99
|
+
srv_host = uri_part.split("@")[1].split("/")[0].split("?")[0]
|
|
100
|
+
else:
|
|
101
|
+
srv_host = (
|
|
102
|
+
uri_part.replace("mongodb+srv://", "")
|
|
103
|
+
.split("/")[0]
|
|
104
|
+
.split("?")[0]
|
|
105
|
+
)
|
|
106
|
+
self.host = srv_host
|
|
107
|
+
self.port = (
|
|
108
|
+
27017 # SRV uses default port, actual ports resolved via DNS
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
# Regular mongodb:// URI parsing
|
|
112
|
+
if "@" in uri_part:
|
|
113
|
+
host_part = uri_part.split("@")[1]
|
|
114
|
+
else:
|
|
115
|
+
host_part = uri_part.replace("mongodb://", "")
|
|
116
|
+
|
|
117
|
+
# Get first host for logging purposes
|
|
118
|
+
if "," in host_part:
|
|
119
|
+
first_host = host_part.split(",")[0].split("/")[0]
|
|
120
|
+
else:
|
|
121
|
+
first_host = host_part.split("/")[0]
|
|
122
|
+
|
|
123
|
+
if ":" in first_host:
|
|
124
|
+
self.host, port_str = first_host.split(":", 1)
|
|
125
|
+
try:
|
|
126
|
+
self.port = int(port_str)
|
|
127
|
+
except ValueError:
|
|
128
|
+
self.port = 27017
|
|
129
|
+
else:
|
|
130
|
+
self.host = first_host
|
|
131
|
+
self.port = 27017
|
|
132
|
+
|
|
133
|
+
# Extract auth info
|
|
134
|
+
parsed = urlparse(self.connection_string)
|
|
135
|
+
self.username = parsed.username
|
|
136
|
+
self.password = parsed.password
|
|
137
|
+
|
|
138
|
+
# Extract query parameters
|
|
139
|
+
self.options = {}
|
|
140
|
+
if query_part:
|
|
141
|
+
params = query_part.split("&")
|
|
142
|
+
for param in params:
|
|
143
|
+
if "=" in param:
|
|
144
|
+
key, value = param.split("=", 1)
|
|
145
|
+
self.options[key] = value
|
|
146
|
+
else:
|
|
147
|
+
# Fall back to urlparse for simple connection strings
|
|
148
|
+
parsed = urlparse(self.connection_string)
|
|
149
|
+
self.host = parsed.hostname or "localhost"
|
|
150
|
+
self.port = parsed.port or 27017
|
|
151
|
+
self.database_name = parsed.path.lstrip("/") or "memori"
|
|
152
|
+
self.username = parsed.username
|
|
153
|
+
self.password = parsed.password
|
|
154
|
+
self.options = {}
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning(f"Failed to parse MongoDB connection string: {e}")
|
|
158
|
+
# Set defaults
|
|
159
|
+
self.host = "localhost"
|
|
160
|
+
self.port = 27017
|
|
161
|
+
self.database_name = "memori"
|
|
162
|
+
self.username = None
|
|
163
|
+
self.password = None
|
|
164
|
+
self.options = {}
|
|
165
|
+
|
|
166
|
+
def get_connection(self) -> MongoClient:
|
|
167
|
+
"""Get MongoDB client connection with support for mongodb+srv DNS seedlist"""
|
|
168
|
+
if self.client is None:
|
|
169
|
+
try:
|
|
170
|
+
# Create MongoDB client with appropriate options
|
|
171
|
+
client_options = {
|
|
172
|
+
"serverSelectionTimeoutMS": 5000, # 5 second timeout
|
|
173
|
+
"connectTimeoutMS": 10000, # 10 second connect timeout
|
|
174
|
+
"socketTimeoutMS": 30000, # 30 second socket timeout
|
|
175
|
+
"maxPoolSize": 50, # Connection pool size
|
|
176
|
+
"retryWrites": True, # Enable retryable writes
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Special handling for mongodb+srv URIs
|
|
180
|
+
is_srv_uri = self.connection_string.startswith("mongodb+srv://")
|
|
181
|
+
|
|
182
|
+
if is_srv_uri:
|
|
183
|
+
# For mongodb+srv URIs, TLS is automatically enabled
|
|
184
|
+
# Don't set directConnection for SRV URIs as they use DNS seedlist discovery
|
|
185
|
+
logger.info(
|
|
186
|
+
"Using MongoDB Atlas DNS seedlist discovery (mongodb+srv)"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Add modern SRV-specific options for 2025
|
|
190
|
+
srv_options = {
|
|
191
|
+
"srvMaxHosts": 0, # No limit on SRV hosts (default)
|
|
192
|
+
"srvServiceName": "mongodb", # Default service name
|
|
193
|
+
}
|
|
194
|
+
client_options.update(srv_options)
|
|
195
|
+
else:
|
|
196
|
+
# For standard mongodb:// URIs
|
|
197
|
+
# Handle replica sets vs single hosts
|
|
198
|
+
if "replicaSet" in self.options:
|
|
199
|
+
logger.info("Using MongoDB replica set connection")
|
|
200
|
+
elif "," in self.connection_string:
|
|
201
|
+
logger.info("Using MongoDB multiple host connection")
|
|
202
|
+
else:
|
|
203
|
+
logger.info("Using MongoDB single host connection")
|
|
204
|
+
|
|
205
|
+
# Add any additional options from connection string (these override defaults)
|
|
206
|
+
client_options.update(self.options)
|
|
207
|
+
|
|
208
|
+
# Never set directConnection for SRV URIs or replica sets
|
|
209
|
+
if is_srv_uri or "replicaSet" in self.options:
|
|
210
|
+
client_options.pop("directConnection", None)
|
|
211
|
+
|
|
212
|
+
logger.debug(f"MongoDB connection options: {client_options}")
|
|
213
|
+
self.client = MongoClient(self.connection_string, **client_options)
|
|
214
|
+
|
|
215
|
+
# Test connection with more detailed logging
|
|
216
|
+
self.client.admin.command("ping")
|
|
217
|
+
|
|
218
|
+
# Get server info for better logging
|
|
219
|
+
try:
|
|
220
|
+
server_info = self.client.server_info()
|
|
221
|
+
version = server_info.get("version", "unknown")
|
|
222
|
+
logger.info(
|
|
223
|
+
f"Connected to MongoDB {version} at {self.host}:{self.port}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if is_srv_uri:
|
|
227
|
+
# Log DNS-resolved hosts for SRV connections
|
|
228
|
+
topology = self.client.topology_description
|
|
229
|
+
hosts = []
|
|
230
|
+
for server in topology.server_descriptions():
|
|
231
|
+
if hasattr(server, "address") and server.address:
|
|
232
|
+
if (
|
|
233
|
+
isinstance(server.address, tuple)
|
|
234
|
+
and len(server.address) >= 2
|
|
235
|
+
):
|
|
236
|
+
hosts.append(
|
|
237
|
+
f"{server.address[0]}:{server.address[1]}"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
hosts.append(str(server.address))
|
|
241
|
+
|
|
242
|
+
if hosts:
|
|
243
|
+
logger.info(f"DNS resolved hosts: {', '.join(hosts)}")
|
|
244
|
+
else:
|
|
245
|
+
logger.info("DNS seedlist discovery completed successfully")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.warning(f"Could not get server info: {e}")
|
|
248
|
+
logger.info(f"Connected to MongoDB at {self.host}:{self.port}")
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
raise DatabaseError(f"Failed to connect to MongoDB: {e}")
|
|
252
|
+
|
|
253
|
+
return self.client
|
|
254
|
+
|
|
255
|
+
def get_database(self) -> Database:
|
|
256
|
+
"""Get MongoDB database"""
|
|
257
|
+
if self.database is None:
|
|
258
|
+
client = self.get_connection()
|
|
259
|
+
self.database = client[self.database_name]
|
|
260
|
+
return self.database
|
|
261
|
+
|
|
262
|
+
def get_collection(self, collection_name: str) -> Collection:
|
|
263
|
+
"""Get MongoDB collection with caching"""
|
|
264
|
+
if collection_name not in self._collections:
|
|
265
|
+
database = self.get_database()
|
|
266
|
+
self._collections[collection_name] = database[collection_name]
|
|
267
|
+
return self._collections[collection_name]
|
|
268
|
+
|
|
269
|
+
def execute_query(
|
|
270
|
+
self, query: str, params: list[Any] | None = None
|
|
271
|
+
) -> list[dict[str, Any]]:
|
|
272
|
+
"""
|
|
273
|
+
Execute a query-like operation in MongoDB
|
|
274
|
+
Note: MongoDB doesn't use SQL, so this is adapted for MongoDB operations
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
# Parse the "query" as a JSON operation for MongoDB
|
|
278
|
+
# This is a compatibility layer for the base interface
|
|
279
|
+
if isinstance(query, str) and query.strip().startswith("{"):
|
|
280
|
+
# Treat as MongoDB operation
|
|
281
|
+
operation = json.loads(query)
|
|
282
|
+
collection_name = operation.get("collection", "memories")
|
|
283
|
+
operation_type = operation.get("operation", "find")
|
|
284
|
+
filter_doc = operation.get("filter", {})
|
|
285
|
+
options = operation.get("options", {})
|
|
286
|
+
|
|
287
|
+
collection = self.get_collection(collection_name)
|
|
288
|
+
|
|
289
|
+
if operation_type == "find":
|
|
290
|
+
cursor = collection.find(filter_doc, **options)
|
|
291
|
+
results = list(cursor)
|
|
292
|
+
# Convert ObjectId to string for JSON serialization
|
|
293
|
+
for result in results:
|
|
294
|
+
if "_id" in result:
|
|
295
|
+
result["_id"] = str(result["_id"])
|
|
296
|
+
return results
|
|
297
|
+
elif operation_type == "aggregate":
|
|
298
|
+
pipeline = operation.get("pipeline", [])
|
|
299
|
+
cursor = collection.aggregate(pipeline, **options)
|
|
300
|
+
results = list(cursor)
|
|
301
|
+
# Convert ObjectId to string for JSON serialization
|
|
302
|
+
for result in results:
|
|
303
|
+
if "_id" in result:
|
|
304
|
+
result["_id"] = str(result["_id"])
|
|
305
|
+
return results
|
|
306
|
+
else:
|
|
307
|
+
raise DatabaseError(
|
|
308
|
+
f"Unsupported MongoDB operation: {operation_type}"
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
# Fallback: treat as a collection name and return all documents
|
|
312
|
+
collection = self.get_collection(query or "memories")
|
|
313
|
+
cursor = collection.find().limit(100) # Limit for safety
|
|
314
|
+
results = list(cursor)
|
|
315
|
+
# Convert ObjectId to string for JSON serialization
|
|
316
|
+
for result in results:
|
|
317
|
+
if "_id" in result:
|
|
318
|
+
result["_id"] = str(result["_id"])
|
|
319
|
+
return results
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
raise DatabaseError(f"Failed to execute MongoDB query: {e}")
|
|
323
|
+
|
|
324
|
+
def execute_insert(self, query: str, params: list[Any] | None = None) -> str:
|
|
325
|
+
"""Execute an insert operation and return the inserted document ID"""
|
|
326
|
+
try:
|
|
327
|
+
if isinstance(query, str) and query.strip().startswith("{"):
|
|
328
|
+
# Parse as MongoDB insert operation
|
|
329
|
+
operation = json.loads(query)
|
|
330
|
+
collection_name = operation.get("collection", "memories")
|
|
331
|
+
document = operation.get("document", {})
|
|
332
|
+
|
|
333
|
+
collection = self.get_collection(collection_name)
|
|
334
|
+
result = collection.insert_one(document)
|
|
335
|
+
return str(result.inserted_id)
|
|
336
|
+
else:
|
|
337
|
+
raise DatabaseError("Invalid insert operation format for MongoDB")
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
raise DatabaseError(f"Failed to execute MongoDB insert: {e}")
|
|
341
|
+
|
|
342
|
+
def execute_update(self, query: str, params: list[Any] | None = None) -> int:
|
|
343
|
+
"""Execute an update operation and return number of modified documents"""
|
|
344
|
+
try:
|
|
345
|
+
if isinstance(query, str) and query.strip().startswith("{"):
|
|
346
|
+
# Parse as MongoDB update operation
|
|
347
|
+
operation = json.loads(query)
|
|
348
|
+
collection_name = operation.get("collection", "memories")
|
|
349
|
+
filter_doc = operation.get("filter", {})
|
|
350
|
+
update_doc = operation.get("update", {})
|
|
351
|
+
options = operation.get("options", {})
|
|
352
|
+
|
|
353
|
+
collection = self.get_collection(collection_name)
|
|
354
|
+
|
|
355
|
+
if operation.get("update_many", False):
|
|
356
|
+
result = collection.update_many(filter_doc, update_doc, **options)
|
|
357
|
+
else:
|
|
358
|
+
result = collection.update_one(filter_doc, update_doc, **options)
|
|
359
|
+
|
|
360
|
+
return result.modified_count
|
|
361
|
+
else:
|
|
362
|
+
raise DatabaseError("Invalid update operation format for MongoDB")
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise DatabaseError(f"Failed to execute MongoDB update: {e}")
|
|
366
|
+
|
|
367
|
+
def execute_delete(self, query: str, params: list[Any] | None = None) -> int:
|
|
368
|
+
"""Execute a delete operation and return number of deleted documents"""
|
|
369
|
+
try:
|
|
370
|
+
if isinstance(query, str) and query.strip().startswith("{"):
|
|
371
|
+
# Parse as MongoDB delete operation
|
|
372
|
+
operation = json.loads(query)
|
|
373
|
+
collection_name = operation.get("collection", "memories")
|
|
374
|
+
filter_doc = operation.get("filter", {})
|
|
375
|
+
options = operation.get("options", {})
|
|
376
|
+
|
|
377
|
+
collection = self.get_collection(collection_name)
|
|
378
|
+
|
|
379
|
+
if operation.get("delete_many", False):
|
|
380
|
+
result = collection.delete_many(filter_doc, **options)
|
|
381
|
+
else:
|
|
382
|
+
result = collection.delete_one(filter_doc, **options)
|
|
383
|
+
|
|
384
|
+
return result.deleted_count
|
|
385
|
+
else:
|
|
386
|
+
raise DatabaseError("Invalid delete operation format for MongoDB")
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
raise DatabaseError(f"Failed to execute MongoDB delete: {e}")
|
|
390
|
+
|
|
391
|
+
def execute_transaction(self, queries: list[tuple[str, list[Any] | None]]) -> bool:
|
|
392
|
+
"""Execute multiple operations in a MongoDB transaction"""
|
|
393
|
+
try:
|
|
394
|
+
client = self.get_connection()
|
|
395
|
+
|
|
396
|
+
# Check if transactions are supported (requires replica set or sharded cluster)
|
|
397
|
+
try:
|
|
398
|
+
with client.start_session() as session:
|
|
399
|
+
with session.start_transaction():
|
|
400
|
+
for query, params in queries:
|
|
401
|
+
# Execute each operation within the transaction
|
|
402
|
+
if "insert" in query.lower():
|
|
403
|
+
self.execute_insert(query, params)
|
|
404
|
+
elif "update" in query.lower():
|
|
405
|
+
self.execute_update(query, params)
|
|
406
|
+
elif "delete" in query.lower():
|
|
407
|
+
self.execute_delete(query, params)
|
|
408
|
+
|
|
409
|
+
# Transaction commits automatically if no exception is raised
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
except OperationFailure as e:
|
|
413
|
+
if "Transaction numbers" in str(e):
|
|
414
|
+
# Transactions not supported, execute operations individually
|
|
415
|
+
logger.warning(
|
|
416
|
+
"Transactions not supported, executing operations individually"
|
|
417
|
+
)
|
|
418
|
+
for query, params in queries:
|
|
419
|
+
if "insert" in query.lower():
|
|
420
|
+
self.execute_insert(query, params)
|
|
421
|
+
elif "update" in query.lower():
|
|
422
|
+
self.execute_update(query, params)
|
|
423
|
+
elif "delete" in query.lower():
|
|
424
|
+
self.execute_delete(query, params)
|
|
425
|
+
return True
|
|
426
|
+
else:
|
|
427
|
+
raise
|
|
428
|
+
|
|
429
|
+
except Exception as e:
|
|
430
|
+
logger.error(f"Transaction failed: {e}")
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
def test_connection(self) -> bool:
|
|
434
|
+
"""Test if the MongoDB connection is working"""
|
|
435
|
+
try:
|
|
436
|
+
client = self.get_connection()
|
|
437
|
+
# Ping the server
|
|
438
|
+
client.admin.command("ping")
|
|
439
|
+
return True
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"MongoDB connection test failed: {e}")
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
def initialize_schema(self, schema_sql: str | None = None):
|
|
445
|
+
"""Initialize MongoDB collections and indexes"""
|
|
446
|
+
try:
|
|
447
|
+
from ..schema_generators.mongodb_schema_generator import (
|
|
448
|
+
MongoDBSchemaGenerator,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
schema_generator = MongoDBSchemaGenerator()
|
|
452
|
+
database = self.get_database()
|
|
453
|
+
|
|
454
|
+
# Create collections with validation rules
|
|
455
|
+
collections_schema = schema_generator.generate_collections_schema()
|
|
456
|
+
for collection_name, schema in collections_schema.items():
|
|
457
|
+
if collection_name not in database.list_collection_names():
|
|
458
|
+
# Create collection with validation
|
|
459
|
+
database.create_collection(
|
|
460
|
+
collection_name,
|
|
461
|
+
validator=schema.get("validator"),
|
|
462
|
+
validationAction=schema.get("validationAction", "error"),
|
|
463
|
+
validationLevel=schema.get("validationLevel", "strict"),
|
|
464
|
+
)
|
|
465
|
+
logger.info(f"Created MongoDB collection: {collection_name}")
|
|
466
|
+
|
|
467
|
+
# Create indexes
|
|
468
|
+
indexes_schema = schema_generator.generate_indexes_schema()
|
|
469
|
+
for collection_name, indexes in indexes_schema.items():
|
|
470
|
+
collection = self.get_collection(collection_name)
|
|
471
|
+
for index in indexes:
|
|
472
|
+
try:
|
|
473
|
+
collection.create_index(
|
|
474
|
+
index["keys"],
|
|
475
|
+
name=index.get("name"),
|
|
476
|
+
unique=index.get("unique", False),
|
|
477
|
+
sparse=index.get("sparse", False),
|
|
478
|
+
background=True, # Create index in background
|
|
479
|
+
)
|
|
480
|
+
logger.debug(f"Created index on {collection_name}: {index}")
|
|
481
|
+
except Exception as e:
|
|
482
|
+
logger.warning(
|
|
483
|
+
f"Failed to create index on {collection_name}: {e}"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
logger.info("MongoDB schema initialization completed")
|
|
487
|
+
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error(f"Failed to initialize MongoDB schema: {e}")
|
|
490
|
+
raise DatabaseError(f"Failed to initialize MongoDB schema: {e}")
|
|
491
|
+
|
|
492
|
+
def supports_full_text_search(self) -> bool:
|
|
493
|
+
"""Check if MongoDB supports text search (always True for MongoDB)"""
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
def supports_vector_search(self) -> bool:
|
|
497
|
+
"""Check if MongoDB Atlas Vector Search is available"""
|
|
498
|
+
try:
|
|
499
|
+
# Check if this is MongoDB Atlas by looking for Atlas-specific features
|
|
500
|
+
client = self.get_connection()
|
|
501
|
+
build_info = client.admin.command("buildInfo")
|
|
502
|
+
|
|
503
|
+
# Atlas typically includes specific modules or version patterns
|
|
504
|
+
# This is a heuristic check - in production you might want to configure this explicitly
|
|
505
|
+
build_info.get("version", "")
|
|
506
|
+
modules = build_info.get("modules", [])
|
|
507
|
+
|
|
508
|
+
# Check if vector search is available (Atlas feature)
|
|
509
|
+
# This is a simplified check - Atlas vector search availability can be complex
|
|
510
|
+
return "atlas" in str(modules).lower() or self._is_atlas_connection()
|
|
511
|
+
|
|
512
|
+
except Exception:
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
def _is_atlas_connection(self) -> bool:
|
|
516
|
+
"""Heuristic to detect if this is an Atlas connection"""
|
|
517
|
+
return (
|
|
518
|
+
"mongodb.net" in self.connection_string.lower()
|
|
519
|
+
or "atlas" in self.connection_string.lower()
|
|
520
|
+
or "cluster" in self.connection_string.lower()
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def create_full_text_index(
|
|
524
|
+
self, table: str, columns: list[str], index_name: str
|
|
525
|
+
) -> str:
|
|
526
|
+
"""Create MongoDB text index"""
|
|
527
|
+
try:
|
|
528
|
+
collection = self.get_collection(table)
|
|
529
|
+
|
|
530
|
+
# Create text index specification
|
|
531
|
+
index_spec = {}
|
|
532
|
+
for column in columns:
|
|
533
|
+
index_spec[column] = "text"
|
|
534
|
+
|
|
535
|
+
collection.create_index(
|
|
536
|
+
list(index_spec.items()), name=index_name, background=True
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return f"Created text index '{index_name}' on collection '{table}'"
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
raise DatabaseError(f"Failed to create text index: {e}")
|
|
543
|
+
|
|
544
|
+
def create_vector_index(
|
|
545
|
+
self,
|
|
546
|
+
collection_name: str,
|
|
547
|
+
vector_field: str,
|
|
548
|
+
dimensions: int,
|
|
549
|
+
similarity: str = "cosine",
|
|
550
|
+
index_name: str | None = None,
|
|
551
|
+
) -> str:
|
|
552
|
+
"""Create MongoDB Atlas Vector Search index"""
|
|
553
|
+
try:
|
|
554
|
+
if not self.supports_vector_search():
|
|
555
|
+
raise DatabaseError(
|
|
556
|
+
"Vector search is not supported in this MongoDB deployment"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
self.get_collection(collection_name)
|
|
560
|
+
|
|
561
|
+
# Vector search index specification for MongoDB Atlas
|
|
562
|
+
|
|
563
|
+
index_name = index_name or f"{vector_field}_vector_index"
|
|
564
|
+
|
|
565
|
+
# Note: Vector search indexes are typically created via Atlas UI or Atlas Admin API
|
|
566
|
+
# This is a placeholder for the actual implementation
|
|
567
|
+
logger.warning(
|
|
568
|
+
"Vector search indexes should be created via MongoDB Atlas UI or Admin API"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return f"Vector index specification created for '{collection_name}.{vector_field}'"
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
raise DatabaseError(f"Failed to create vector index: {e}")
|
|
575
|
+
|
|
576
|
+
def get_database_info(self) -> dict[str, Any]:
|
|
577
|
+
"""Get MongoDB database information and capabilities"""
|
|
578
|
+
try:
|
|
579
|
+
client = self.get_connection()
|
|
580
|
+
database = self.get_database()
|
|
581
|
+
|
|
582
|
+
info = {}
|
|
583
|
+
|
|
584
|
+
# Server information
|
|
585
|
+
server_info = client.server_info()
|
|
586
|
+
info["version"] = server_info.get("version", "unknown")
|
|
587
|
+
info["database_type"] = self.database_type.value
|
|
588
|
+
info["database_name"] = self.database_name
|
|
589
|
+
info["connection_string"] = (
|
|
590
|
+
self.connection_string.replace(
|
|
591
|
+
f"{self.username}:{self.password}@", "***:***@"
|
|
592
|
+
)
|
|
593
|
+
if self.username and self.password
|
|
594
|
+
else self.connection_string
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Database stats
|
|
598
|
+
try:
|
|
599
|
+
stats = database.command("dbStats")
|
|
600
|
+
info["collections_count"] = stats.get("collections", 0)
|
|
601
|
+
info["data_size"] = stats.get("dataSize", 0)
|
|
602
|
+
info["storage_size"] = stats.get("storageSize", 0)
|
|
603
|
+
info["indexes_count"] = stats.get("indexes", 0)
|
|
604
|
+
except Exception:
|
|
605
|
+
pass
|
|
606
|
+
|
|
607
|
+
# Capabilities
|
|
608
|
+
info["full_text_search_support"] = True
|
|
609
|
+
info["vector_search_support"] = self.supports_vector_search()
|
|
610
|
+
info["transactions_support"] = self._check_transactions_support()
|
|
611
|
+
|
|
612
|
+
# Replica set information
|
|
613
|
+
try:
|
|
614
|
+
replica_config = client.admin.command("replSetGetStatus")
|
|
615
|
+
info["replica_set"] = replica_config.get("set", "Not in replica set")
|
|
616
|
+
except Exception:
|
|
617
|
+
info["replica_set"] = "Standalone"
|
|
618
|
+
|
|
619
|
+
return info
|
|
620
|
+
|
|
621
|
+
except Exception as e:
|
|
622
|
+
logger.warning(f"Could not get MongoDB database info: {e}")
|
|
623
|
+
return {
|
|
624
|
+
"database_type": self.database_type.value,
|
|
625
|
+
"version": "unknown",
|
|
626
|
+
"full_text_search_support": True,
|
|
627
|
+
"vector_search_support": False,
|
|
628
|
+
"error": str(e),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
def _check_transactions_support(self) -> bool:
|
|
632
|
+
"""Check if MongoDB deployment supports transactions"""
|
|
633
|
+
try:
|
|
634
|
+
client = self.get_connection()
|
|
635
|
+
with client.start_session() as session:
|
|
636
|
+
with session.start_transaction():
|
|
637
|
+
# Just test if we can start a transaction
|
|
638
|
+
pass
|
|
639
|
+
return True
|
|
640
|
+
except Exception:
|
|
641
|
+
return False
|
|
642
|
+
|
|
643
|
+
def close(self):
|
|
644
|
+
"""Close MongoDB connection"""
|
|
645
|
+
if self.client:
|
|
646
|
+
self.client.close()
|
|
647
|
+
self.client = None
|
|
648
|
+
self.database = None
|
|
649
|
+
self._collections.clear()
|
|
650
|
+
logger.info("MongoDB connection closed")
|
|
651
|
+
|
|
652
|
+
def __del__(self):
|
|
653
|
+
"""Cleanup when connector is destroyed"""
|
|
654
|
+
self.close()
|