memorisdk 1.0.1__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.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +416 -60
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1676 -534
- 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 +548 -0
- memori/database/sqlalchemy_manager.py +839 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- 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.1.dist-info → memorisdk-2.0.0.dist-info}/METADATA +144 -34
- memorisdk-2.0.0.dist-info/RECORD +67 -0
- memorisdk-1.0.1.dist-info/RECORD +0 -44
- memorisdk-1.0.1.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -6,32 +6,143 @@ from typing import Any, Dict, List, Optional
|
|
|
6
6
|
|
|
7
7
|
from loguru import logger
|
|
8
8
|
|
|
9
|
-
from ...utils.exceptions import DatabaseError
|
|
9
|
+
from ...utils.exceptions import DatabaseError, ValidationError
|
|
10
|
+
from .base_connector import BaseDatabaseConnector, DatabaseType
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
class PostgreSQLConnector:
|
|
13
|
+
class PostgreSQLConnector(BaseDatabaseConnector):
|
|
13
14
|
"""PostgreSQL database connector"""
|
|
14
15
|
|
|
15
|
-
def __init__(self,
|
|
16
|
+
def __init__(self, connection_config):
|
|
16
17
|
"""Initialize PostgreSQL connector"""
|
|
17
|
-
|
|
18
|
+
if isinstance(connection_config, str):
|
|
19
|
+
self.connection_string = connection_config
|
|
20
|
+
else:
|
|
21
|
+
self.connection_string = self._build_connection_string(connection_config)
|
|
18
22
|
self._psycopg2 = None
|
|
19
23
|
self._setup_psycopg2()
|
|
24
|
+
self._ensure_database_exists()
|
|
25
|
+
super().__init__(connection_config)
|
|
20
26
|
|
|
21
27
|
def _setup_psycopg2(self):
|
|
22
28
|
"""Setup psycopg2 connection"""
|
|
23
29
|
try:
|
|
24
30
|
import psycopg2
|
|
25
31
|
import psycopg2.extras
|
|
32
|
+
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
|
26
33
|
|
|
27
34
|
self._psycopg2 = psycopg2
|
|
28
35
|
self._extras = psycopg2.extras
|
|
36
|
+
self.ISOLATION_LEVEL_AUTOCOMMIT = ISOLATION_LEVEL_AUTOCOMMIT
|
|
29
37
|
except ImportError:
|
|
30
38
|
raise DatabaseError(
|
|
31
39
|
"psycopg2 is required for PostgreSQL support. "
|
|
32
40
|
"Install it with: pip install psycopg2-binary"
|
|
33
41
|
)
|
|
34
42
|
|
|
43
|
+
def _detect_database_type(self) -> DatabaseType:
|
|
44
|
+
"""Detect database type from connection config"""
|
|
45
|
+
return DatabaseType.POSTGRESQL
|
|
46
|
+
|
|
47
|
+
def _build_connection_string(self, config: Dict[str, Any]) -> str:
|
|
48
|
+
"""Build PostgreSQL connection string from config dict"""
|
|
49
|
+
try:
|
|
50
|
+
user = config.get("user", "postgres")
|
|
51
|
+
password = config.get("password", "")
|
|
52
|
+
host = config.get("host", "localhost")
|
|
53
|
+
port = config.get("port", 5432)
|
|
54
|
+
database = config.get("database")
|
|
55
|
+
|
|
56
|
+
if not database:
|
|
57
|
+
raise ValidationError("Database name is required")
|
|
58
|
+
|
|
59
|
+
if password:
|
|
60
|
+
return f"postgresql://{user}:{password}@{host}:{port}/{database}"
|
|
61
|
+
else:
|
|
62
|
+
return f"postgresql://{user}@{host}:{port}/{database}"
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise DatabaseError(f"Invalid PostgreSQL configuration: {e}")
|
|
65
|
+
|
|
66
|
+
def _parse_connection_string(self):
|
|
67
|
+
"""Parse connection string to extract components"""
|
|
68
|
+
import re
|
|
69
|
+
|
|
70
|
+
# Parse PostgreSQL connection string
|
|
71
|
+
# Format: postgresql://user:password@host:port/database
|
|
72
|
+
# or: postgresql+psycopg2://user:password@host:port/database
|
|
73
|
+
pattern = r"postgresql(?:\+psycopg2)?://(?:([^:]+)(?::([^@]+))?@)?([^:/]+)(?::(\d+))?/(.+)"
|
|
74
|
+
match = re.match(pattern, self.connection_string)
|
|
75
|
+
|
|
76
|
+
if not match:
|
|
77
|
+
raise DatabaseError(
|
|
78
|
+
f"Invalid PostgreSQL connection string: {self.connection_string}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
user, password, host, port, database = match.groups()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"user": user or "postgres",
|
|
85
|
+
"password": password or "",
|
|
86
|
+
"host": host or "localhost",
|
|
87
|
+
"port": port or "5432",
|
|
88
|
+
"database": database,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def _ensure_database_exists(self):
|
|
92
|
+
"""Ensure the database exists, create if it doesn't"""
|
|
93
|
+
params = self._parse_connection_string()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Connect to default 'postgres' database to check/create target database
|
|
97
|
+
admin_conn_params = {
|
|
98
|
+
"host": params["host"],
|
|
99
|
+
"port": params["port"],
|
|
100
|
+
"user": params["user"],
|
|
101
|
+
"database": "postgres",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if params["password"]:
|
|
105
|
+
admin_conn_params["password"] = params["password"]
|
|
106
|
+
|
|
107
|
+
conn = self._psycopg2.connect(**admin_conn_params)
|
|
108
|
+
conn.set_isolation_level(self.ISOLATION_LEVEL_AUTOCOMMIT)
|
|
109
|
+
cursor = conn.cursor()
|
|
110
|
+
|
|
111
|
+
# Check if database exists
|
|
112
|
+
cursor.execute(
|
|
113
|
+
"SELECT 1 FROM pg_database WHERE datname = %s", (params["database"],)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if not cursor.fetchone():
|
|
117
|
+
# Database doesn't exist, create it
|
|
118
|
+
# Use SQL identifier for database name (can't use parameter substitution for DDL)
|
|
119
|
+
from psycopg2 import sql
|
|
120
|
+
|
|
121
|
+
cursor.execute(
|
|
122
|
+
sql.SQL("CREATE DATABASE {}").format(
|
|
123
|
+
sql.Identifier(params["database"])
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
logger.info(f"Created PostgreSQL database: {params['database']}")
|
|
127
|
+
|
|
128
|
+
# Set UTF-8 encoding
|
|
129
|
+
cursor.execute(
|
|
130
|
+
sql.SQL("ALTER DATABASE {} SET client_encoding TO 'UTF8'").format(
|
|
131
|
+
sql.Identifier(params["database"])
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
cursor.close()
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
138
|
+
except self._psycopg2.OperationalError as e:
|
|
139
|
+
# If we can't connect to postgres database, try connecting directly
|
|
140
|
+
# This might work if the target database already exists
|
|
141
|
+
logger.warning(f"Could not connect to postgres database: {e}")
|
|
142
|
+
logger.info("Attempting direct connection to target database...")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning(f"Could not ensure database exists: {e}")
|
|
145
|
+
|
|
35
146
|
def get_connection(self):
|
|
36
147
|
"""Get PostgreSQL connection"""
|
|
37
148
|
try:
|
|
@@ -156,3 +267,165 @@ class PostgreSQLConnector:
|
|
|
156
267
|
except Exception as e:
|
|
157
268
|
logger.error(f"Connection test failed: {e}")
|
|
158
269
|
return False
|
|
270
|
+
|
|
271
|
+
def initialize_schema(self, schema_sql: Optional[str] = None):
|
|
272
|
+
"""Initialize PostgreSQL database schema"""
|
|
273
|
+
try:
|
|
274
|
+
if not schema_sql:
|
|
275
|
+
# Use PostgreSQL-specific schema
|
|
276
|
+
try:
|
|
277
|
+
from ..schema_generators.postgresql_schema_generator import ( # type: ignore
|
|
278
|
+
PostgreSQLSchemaGenerator,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
schema_generator = PostgreSQLSchemaGenerator()
|
|
282
|
+
schema_sql = schema_generator.generate_full_schema()
|
|
283
|
+
except ImportError:
|
|
284
|
+
# Fall back to None - let base functionality handle it
|
|
285
|
+
schema_sql = None
|
|
286
|
+
|
|
287
|
+
# Execute schema using transaction
|
|
288
|
+
with self.get_connection() as conn:
|
|
289
|
+
with conn.cursor() as cursor:
|
|
290
|
+
try:
|
|
291
|
+
# Split schema into individual statements
|
|
292
|
+
statements = self._split_postgresql_statements(schema_sql)
|
|
293
|
+
|
|
294
|
+
for statement in statements:
|
|
295
|
+
statement = statement.strip()
|
|
296
|
+
if statement and not statement.startswith("--"):
|
|
297
|
+
cursor.execute(statement)
|
|
298
|
+
|
|
299
|
+
conn.commit()
|
|
300
|
+
logger.info(
|
|
301
|
+
"PostgreSQL database schema initialized successfully"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
conn.rollback()
|
|
306
|
+
logger.error(f"Failed to initialize PostgreSQL schema: {e}")
|
|
307
|
+
raise DatabaseError(f"Schema initialization failed: {e}")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to initialize PostgreSQL schema: {e}")
|
|
311
|
+
raise DatabaseError(f"Failed to initialize PostgreSQL schema: {e}")
|
|
312
|
+
|
|
313
|
+
def supports_full_text_search(self) -> bool:
|
|
314
|
+
"""Check if PostgreSQL supports full-text search"""
|
|
315
|
+
try:
|
|
316
|
+
with self.get_connection() as conn:
|
|
317
|
+
with conn.cursor() as cursor:
|
|
318
|
+
# Test tsvector functionality
|
|
319
|
+
cursor.execute(
|
|
320
|
+
"SELECT to_tsvector('english', 'test') @@ plainto_tsquery('english', 'test')"
|
|
321
|
+
)
|
|
322
|
+
result = cursor.fetchone()
|
|
323
|
+
return result[0] if result else False
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.warning(f"Could not determine PostgreSQL FTS support: {e}")
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
def create_full_text_index(
|
|
329
|
+
self, table: str, columns: List[str], index_name: str
|
|
330
|
+
) -> str:
|
|
331
|
+
"""Create PostgreSQL GIN index for full-text search"""
|
|
332
|
+
# Validate inputs
|
|
333
|
+
try:
|
|
334
|
+
from ...utils.input_validator import InputValidator
|
|
335
|
+
|
|
336
|
+
table = InputValidator.sanitize_sql_identifier(table)
|
|
337
|
+
index_name = InputValidator.sanitize_sql_identifier(index_name)
|
|
338
|
+
for col in columns:
|
|
339
|
+
InputValidator.sanitize_sql_identifier(col)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
raise DatabaseError(f"Invalid index parameters: {e}")
|
|
342
|
+
|
|
343
|
+
# Create tsvector expression
|
|
344
|
+
tsvector_expr = " || ' ' || ".join(columns)
|
|
345
|
+
return f"CREATE INDEX {index_name} ON {table} USING gin(to_tsvector('english', {tsvector_expr}))"
|
|
346
|
+
|
|
347
|
+
def get_database_info(self) -> Dict[str, Any]:
|
|
348
|
+
"""Get PostgreSQL database information and capabilities"""
|
|
349
|
+
try:
|
|
350
|
+
with self.get_connection() as conn:
|
|
351
|
+
with conn.cursor(cursor_factory=self._extras.RealDictCursor) as cursor:
|
|
352
|
+
info = {}
|
|
353
|
+
|
|
354
|
+
# Basic version info
|
|
355
|
+
cursor.execute("SELECT version() as version")
|
|
356
|
+
version_result = cursor.fetchone()
|
|
357
|
+
info["version"] = (
|
|
358
|
+
version_result["version"] if version_result else "unknown"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Database name
|
|
362
|
+
cursor.execute("SELECT current_database() as db_name")
|
|
363
|
+
db_result = cursor.fetchone()
|
|
364
|
+
info["database"] = db_result["db_name"] if db_result else "unknown"
|
|
365
|
+
|
|
366
|
+
# Extensions info
|
|
367
|
+
cursor.execute("SELECT extname FROM pg_extension")
|
|
368
|
+
extensions = cursor.fetchall()
|
|
369
|
+
info["extensions"] = (
|
|
370
|
+
[ext["extname"] for ext in extensions] if extensions else []
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Check for full-text search capabilities
|
|
374
|
+
info["database_type"] = self.database_type.value
|
|
375
|
+
info["fulltext_support"] = self.supports_full_text_search()
|
|
376
|
+
|
|
377
|
+
# Connection info
|
|
378
|
+
info["connection_string"] = (
|
|
379
|
+
self.connection_string.split("@")[1]
|
|
380
|
+
if "@" in self.connection_string
|
|
381
|
+
else "unknown"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return info
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.warning(f"Could not get PostgreSQL database info: {e}")
|
|
388
|
+
return {
|
|
389
|
+
"database_type": self.database_type.value,
|
|
390
|
+
"version": "unknown",
|
|
391
|
+
"fulltext_support": False,
|
|
392
|
+
"error": str(e),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def _split_postgresql_statements(self, schema_sql: str) -> List[str]:
|
|
396
|
+
"""Split SQL schema into individual statements handling PostgreSQL syntax"""
|
|
397
|
+
statements = []
|
|
398
|
+
current_statement = []
|
|
399
|
+
in_function = False
|
|
400
|
+
|
|
401
|
+
for line in schema_sql.split("\n"):
|
|
402
|
+
line = line.strip()
|
|
403
|
+
|
|
404
|
+
# Skip comments and empty lines
|
|
405
|
+
if not line or line.startswith("--"):
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
# Track function/procedure boundaries
|
|
409
|
+
if line.upper().startswith(
|
|
410
|
+
("CREATE FUNCTION", "CREATE OR REPLACE FUNCTION", "CREATE PROCEDURE")
|
|
411
|
+
):
|
|
412
|
+
in_function = True
|
|
413
|
+
elif line.upper().startswith("$$") and in_function:
|
|
414
|
+
in_function = False
|
|
415
|
+
|
|
416
|
+
current_statement.append(line)
|
|
417
|
+
|
|
418
|
+
# PostgreSQL uses semicolon to end statements (except within functions)
|
|
419
|
+
if line.endswith(";") and not in_function:
|
|
420
|
+
statement = " ".join(current_statement)
|
|
421
|
+
if statement.strip():
|
|
422
|
+
statements.append(statement)
|
|
423
|
+
current_statement = []
|
|
424
|
+
|
|
425
|
+
# Add any remaining statement
|
|
426
|
+
if current_statement:
|
|
427
|
+
statement = " ".join(current_statement)
|
|
428
|
+
if statement.strip():
|
|
429
|
+
statements.append(statement)
|
|
430
|
+
|
|
431
|
+
return statements
|
|
@@ -9,15 +9,34 @@ from typing import Any, Dict, List, Optional
|
|
|
9
9
|
from loguru import logger
|
|
10
10
|
|
|
11
11
|
from ...utils.exceptions import DatabaseError
|
|
12
|
+
from ...utils.input_validator import InputValidator
|
|
13
|
+
from .base_connector import BaseDatabaseConnector, DatabaseType
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class SQLiteConnector:
|
|
16
|
+
class SQLiteConnector(BaseDatabaseConnector):
|
|
15
17
|
"""SQLite database connector with FTS5 support"""
|
|
16
18
|
|
|
17
|
-
def __init__(self,
|
|
19
|
+
def __init__(self, connection_config):
|
|
18
20
|
"""Initialize SQLite connector"""
|
|
19
|
-
|
|
21
|
+
if isinstance(connection_config, str):
|
|
22
|
+
self.db_path = self._parse_db_path(connection_config)
|
|
23
|
+
else:
|
|
24
|
+
self.db_path = connection_config.get("database", ":memory:")
|
|
20
25
|
self._ensure_directory_exists()
|
|
26
|
+
super().__init__(connection_config)
|
|
27
|
+
|
|
28
|
+
def _detect_database_type(self) -> DatabaseType:
|
|
29
|
+
"""Detect database type from connection config"""
|
|
30
|
+
return DatabaseType.SQLITE
|
|
31
|
+
|
|
32
|
+
def _parse_db_path(self, connection_string: str) -> str:
|
|
33
|
+
"""Parse SQLite connection string to get database path"""
|
|
34
|
+
if connection_string.startswith("sqlite:///"):
|
|
35
|
+
return connection_string.replace("sqlite:///", "")
|
|
36
|
+
elif connection_string.startswith("sqlite://"):
|
|
37
|
+
return connection_string.replace("sqlite://", "")
|
|
38
|
+
else:
|
|
39
|
+
return connection_string
|
|
21
40
|
|
|
22
41
|
def _ensure_directory_exists(self):
|
|
23
42
|
"""Ensure the database directory exists"""
|
|
@@ -146,3 +165,159 @@ class SQLiteConnector:
|
|
|
146
165
|
except Exception as e:
|
|
147
166
|
logger.error(f"Connection test failed: {e}")
|
|
148
167
|
return False
|
|
168
|
+
|
|
169
|
+
def initialize_schema(self, schema_sql: Optional[str] = None):
|
|
170
|
+
"""Initialize SQLite database schema"""
|
|
171
|
+
try:
|
|
172
|
+
if not schema_sql:
|
|
173
|
+
# Use SQLite-specific schema
|
|
174
|
+
try:
|
|
175
|
+
from ..schema_generators.sqlite_schema_generator import ( # type: ignore
|
|
176
|
+
SQLiteSchemaGenerator,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
schema_generator = SQLiteSchemaGenerator()
|
|
180
|
+
schema_sql = schema_generator.generate_full_schema()
|
|
181
|
+
except ImportError:
|
|
182
|
+
# Fall back to None - let base functionality handle it
|
|
183
|
+
schema_sql = None
|
|
184
|
+
|
|
185
|
+
# Execute schema using transaction
|
|
186
|
+
with self.get_connection() as conn:
|
|
187
|
+
cursor = conn.cursor()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Split schema into individual statements
|
|
191
|
+
statements = self._split_sqlite_statements(schema_sql)
|
|
192
|
+
|
|
193
|
+
for statement in statements:
|
|
194
|
+
statement = statement.strip()
|
|
195
|
+
if statement and not statement.startswith("--"):
|
|
196
|
+
cursor.execute(statement)
|
|
197
|
+
|
|
198
|
+
conn.commit()
|
|
199
|
+
logger.info("SQLite database schema initialized successfully")
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
conn.rollback()
|
|
203
|
+
logger.error(f"Failed to initialize SQLite schema: {e}")
|
|
204
|
+
raise DatabaseError(f"Schema initialization failed: {e}")
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"Failed to initialize SQLite schema: {e}")
|
|
208
|
+
raise DatabaseError(f"Failed to initialize SQLite schema: {e}")
|
|
209
|
+
|
|
210
|
+
def supports_full_text_search(self) -> bool:
|
|
211
|
+
"""Check if SQLite supports FTS5"""
|
|
212
|
+
try:
|
|
213
|
+
with self.get_connection() as conn:
|
|
214
|
+
cursor = conn.cursor()
|
|
215
|
+
cursor.execute("CREATE VIRTUAL TABLE fts_test USING fts5(content)")
|
|
216
|
+
cursor.execute("DROP TABLE fts_test")
|
|
217
|
+
return True
|
|
218
|
+
except sqlite3.OperationalError:
|
|
219
|
+
return False
|
|
220
|
+
except Exception:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def create_full_text_index(
|
|
224
|
+
self, table: str, columns: List[str], index_name: str
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Create SQLite FTS5 virtual table"""
|
|
227
|
+
# Validate inputs
|
|
228
|
+
try:
|
|
229
|
+
table = InputValidator.sanitize_sql_identifier(table)
|
|
230
|
+
index_name = InputValidator.sanitize_sql_identifier(index_name)
|
|
231
|
+
for col in columns:
|
|
232
|
+
InputValidator.sanitize_sql_identifier(col)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise DatabaseError(f"Invalid index parameters: {e}")
|
|
235
|
+
|
|
236
|
+
columns_str = ", ".join(columns)
|
|
237
|
+
return f"CREATE VIRTUAL TABLE {index_name} USING fts5({columns_str})"
|
|
238
|
+
|
|
239
|
+
def get_database_info(self) -> Dict[str, Any]:
|
|
240
|
+
"""Get SQLite database information and capabilities"""
|
|
241
|
+
try:
|
|
242
|
+
with self.get_connection() as conn:
|
|
243
|
+
cursor = conn.cursor()
|
|
244
|
+
|
|
245
|
+
info = {}
|
|
246
|
+
|
|
247
|
+
# SQLite version
|
|
248
|
+
cursor.execute("SELECT sqlite_version() as version")
|
|
249
|
+
version_result = cursor.fetchone()
|
|
250
|
+
info["version"] = version_result[0] if version_result else "unknown"
|
|
251
|
+
|
|
252
|
+
# Database file info
|
|
253
|
+
info["database_file"] = self.db_path
|
|
254
|
+
info["database_type"] = self.database_type.value
|
|
255
|
+
|
|
256
|
+
# Check capabilities
|
|
257
|
+
info["fts5_support"] = self.supports_full_text_search()
|
|
258
|
+
|
|
259
|
+
# Pragma settings
|
|
260
|
+
cursor.execute("PRAGMA journal_mode")
|
|
261
|
+
journal_mode = cursor.fetchone()
|
|
262
|
+
info["journal_mode"] = journal_mode[0] if journal_mode else "unknown"
|
|
263
|
+
|
|
264
|
+
cursor.execute("PRAGMA synchronous")
|
|
265
|
+
synchronous = cursor.fetchone()
|
|
266
|
+
info["synchronous"] = synchronous[0] if synchronous else "unknown"
|
|
267
|
+
|
|
268
|
+
cursor.execute("PRAGMA cache_size")
|
|
269
|
+
cache_size = cursor.fetchone()
|
|
270
|
+
info["cache_size"] = cache_size[0] if cache_size else "unknown"
|
|
271
|
+
|
|
272
|
+
return info
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.warning(f"Could not get SQLite database info: {e}")
|
|
276
|
+
return {
|
|
277
|
+
"database_type": self.database_type.value,
|
|
278
|
+
"version": "unknown",
|
|
279
|
+
"fts5_support": False,
|
|
280
|
+
"error": str(e),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def _split_sqlite_statements(self, schema_sql: str) -> List[str]:
|
|
284
|
+
"""Split SQL schema into individual statements handling SQLite syntax"""
|
|
285
|
+
statements = []
|
|
286
|
+
current_statement = []
|
|
287
|
+
in_trigger = False
|
|
288
|
+
|
|
289
|
+
for line in schema_sql.split("\n"):
|
|
290
|
+
line = line.strip()
|
|
291
|
+
|
|
292
|
+
# Skip comments and empty lines
|
|
293
|
+
if not line or line.startswith("--"):
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Track trigger boundaries
|
|
297
|
+
if line.upper().startswith("CREATE TRIGGER"):
|
|
298
|
+
in_trigger = True
|
|
299
|
+
elif line.upper() == "END;" and in_trigger:
|
|
300
|
+
current_statement.append(line)
|
|
301
|
+
statement = " ".join(current_statement)
|
|
302
|
+
if statement.strip():
|
|
303
|
+
statements.append(statement)
|
|
304
|
+
current_statement = []
|
|
305
|
+
in_trigger = False
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
current_statement.append(line)
|
|
309
|
+
|
|
310
|
+
# SQLite uses semicolon to end statements (except within triggers)
|
|
311
|
+
if line.endswith(";") and not in_trigger:
|
|
312
|
+
statement = " ".join(current_statement)
|
|
313
|
+
if statement.strip():
|
|
314
|
+
statements.append(statement)
|
|
315
|
+
current_statement = []
|
|
316
|
+
|
|
317
|
+
# Add any remaining statement
|
|
318
|
+
if current_statement:
|
|
319
|
+
statement = " ".join(current_statement)
|
|
320
|
+
if statement.strip():
|
|
321
|
+
statements.append(statement)
|
|
322
|
+
|
|
323
|
+
return statements
|