Spaces:
Running
Running
Commit ·
55ae875
1
Parent(s): 7d1ad3f
ok
Browse files- app/api/v1/database.py +24 -1
- app/core/database/base.py +2 -1
- app/models/schemas.py +39 -0
- app/services/database_service.py +78 -0
app/api/v1/database.py
CHANGED
|
@@ -6,13 +6,36 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|
| 6 |
|
| 7 |
from app.api.deps import require_auth
|
| 8 |
from app.core.logger import get_logger
|
| 9 |
-
from app.models.schemas import DatabaseQueryRequest, DatabaseQueryResponse
|
| 10 |
from app.services.database_service import DatabaseService
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
_logger = get_logger(__name__)
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
@router.post(
|
| 17 |
"/database/query",
|
| 18 |
response_model=DatabaseQueryResponse,
|
|
|
|
| 6 |
|
| 7 |
from app.api.deps import require_auth
|
| 8 |
from app.core.logger import get_logger
|
| 9 |
+
from app.models.schemas import DatabaseQueryRequest, DatabaseQueryResponse, DatabaseValidateRequest, DatabaseValidateResponse
|
| 10 |
from app.services.database_service import DatabaseService
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
_logger = get_logger(__name__)
|
| 14 |
|
| 15 |
|
| 16 |
+
@router.post(
|
| 17 |
+
"/database/validate",
|
| 18 |
+
response_model=DatabaseValidateResponse,
|
| 19 |
+
summary="Validate database connection and optionally check table/collection existence",
|
| 20 |
+
)
|
| 21 |
+
async def validate_database(
|
| 22 |
+
body: DatabaseValidateRequest,
|
| 23 |
+
token: Annotated[str, Depends(require_auth)],
|
| 24 |
+
db_service: Annotated[DatabaseService, Depends()] = None,
|
| 25 |
+
) -> DatabaseValidateResponse:
|
| 26 |
+
if db_service is None:
|
| 27 |
+
db_service = DatabaseService()
|
| 28 |
+
_logger.info("Database validate request: %s", body.connection.safe_repr())
|
| 29 |
+
try:
|
| 30 |
+
return await db_service.validate_connection(body)
|
| 31 |
+
except Exception as exc:
|
| 32 |
+
_logger.error("Unexpected error validating database: %s", exc)
|
| 33 |
+
raise HTTPException(
|
| 34 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 35 |
+
detail={"success": False, "message": f"Internal error: {exc}"},
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
@router.post(
|
| 40 |
"/database/query",
|
| 41 |
response_model=DatabaseQueryResponse,
|
app/core/database/base.py
CHANGED
|
@@ -19,6 +19,7 @@ class ConnectionConfig:
|
|
| 19 |
database: str
|
| 20 |
username: str
|
| 21 |
password: str
|
|
|
|
| 22 |
ssl_enabled: bool = False
|
| 23 |
ssl_ca_cert: str | None = None
|
| 24 |
ssl_cert: str | None = None
|
|
@@ -36,7 +37,7 @@ class ConnectionConfig:
|
|
| 36 |
return (
|
| 37 |
f"ConnectionConfig(db_type={self.db_type}, host={self.host}, "
|
| 38 |
f"port={self.port}, database={self.database}, "
|
| 39 |
-
f"username={self.username}, ssl={self.ssl_enabled})"
|
| 40 |
)
|
| 41 |
|
| 42 |
|
|
|
|
| 19 |
database: str
|
| 20 |
username: str
|
| 21 |
password: str
|
| 22 |
+
selected_schema: str = "public"
|
| 23 |
ssl_enabled: bool = False
|
| 24 |
ssl_ca_cert: str | None = None
|
| 25 |
ssl_cert: str | None = None
|
|
|
|
| 37 |
return (
|
| 38 |
f"ConnectionConfig(db_type={self.db_type}, host={self.host}, "
|
| 39 |
f"port={self.port}, database={self.database}, "
|
| 40 |
+
f"selected_schema={self.selected_schema}, username={self.username}, ssl={self.ssl_enabled})"
|
| 41 |
)
|
| 42 |
|
| 43 |
|
app/models/schemas.py
CHANGED
|
@@ -162,6 +162,7 @@ class DatabaseConnection(BaseModel):
|
|
| 162 |
database: str
|
| 163 |
username: str
|
| 164 |
password: str = Field(repr=False)
|
|
|
|
| 165 |
ssl: SSLConfig = SSLConfig()
|
| 166 |
|
| 167 |
@field_validator("host")
|
|
@@ -231,6 +232,7 @@ class DatabaseQueryRequest(BaseModel):
|
|
| 231 |
"database": self.connection.database,
|
| 232 |
"username": self.connection.username,
|
| 233 |
"password": self.connection.password,
|
|
|
|
| 234 |
"ssl_enabled": self.connection.ssl.enabled,
|
| 235 |
"ssl_ca_cert": self.connection.ssl.ca_cert,
|
| 236 |
"ssl_cert": self.connection.ssl.cert,
|
|
@@ -264,6 +266,43 @@ class DatabaseQueryResponse(BaseModel):
|
|
| 264 |
error: Optional[DatabaseQueryError] = None
|
| 265 |
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
class EmbeddingItem(BaseModel):
|
| 268 |
success: bool
|
| 269 |
time_ms: float
|
|
|
|
| 162 |
database: str
|
| 163 |
username: str
|
| 164 |
password: str = Field(repr=False)
|
| 165 |
+
selected_schema: str = Field(default="public", description="PostgreSQL schema (ignored for MySQL/MongoDB)")
|
| 166 |
ssl: SSLConfig = SSLConfig()
|
| 167 |
|
| 168 |
@field_validator("host")
|
|
|
|
| 232 |
"database": self.connection.database,
|
| 233 |
"username": self.connection.username,
|
| 234 |
"password": self.connection.password,
|
| 235 |
+
"selected_schema": self.connection.selected_schema,
|
| 236 |
"ssl_enabled": self.connection.ssl.enabled,
|
| 237 |
"ssl_ca_cert": self.connection.ssl.ca_cert,
|
| 238 |
"ssl_cert": self.connection.ssl.cert,
|
|
|
|
| 266 |
error: Optional[DatabaseQueryError] = None
|
| 267 |
|
| 268 |
|
| 269 |
+
class TableValidationResult(BaseModel):
|
| 270 |
+
name: str
|
| 271 |
+
exists: bool
|
| 272 |
+
error: Optional[str] = None
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
class DatabaseValidateRequest(BaseModel):
|
| 276 |
+
db_type: Literal["mysql", "postgresql", "mongodb"]
|
| 277 |
+
connection: DatabaseConnection
|
| 278 |
+
selected_schema: str = Field(default="public", description="PostgreSQL schema (ignored for MySQL/MongoDB)")
|
| 279 |
+
table_or_collection_names: List[str] = Field(default_factory=list, description="Optional list of tables/collections to check for existence")
|
| 280 |
+
|
| 281 |
+
def to_connection_config(self) -> Dict[str, Any]:
|
| 282 |
+
return {
|
| 283 |
+
"db_type": self.db_type,
|
| 284 |
+
"host": self.connection.host,
|
| 285 |
+
"port": self.connection.port or {"mysql": 3306, "postgresql": 5432, "mongodb": 27017}[self.db_type],
|
| 286 |
+
"database": self.connection.database,
|
| 287 |
+
"username": self.connection.username,
|
| 288 |
+
"password": self.connection.password,
|
| 289 |
+
"selected_schema": self.selected_schema,
|
| 290 |
+
"ssl_enabled": self.connection.ssl.enabled,
|
| 291 |
+
"ssl_ca_cert": self.connection.ssl.ca_cert,
|
| 292 |
+
"ssl_cert": self.connection.ssl.cert,
|
| 293 |
+
"ssl_key": self.connection.ssl.key,
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class DatabaseValidateResponse(BaseModel):
|
| 298 |
+
success: bool
|
| 299 |
+
time_ms: float
|
| 300 |
+
message: str
|
| 301 |
+
connection_details: str
|
| 302 |
+
error_message: Optional[str] = None
|
| 303 |
+
tables: List[TableValidationResult] = []
|
| 304 |
+
|
| 305 |
+
|
| 306 |
class EmbeddingItem(BaseModel):
|
| 307 |
success: bool
|
| 308 |
time_ms: float
|
app/services/database_service.py
CHANGED
|
@@ -10,7 +10,10 @@ from app.models.schemas import (
|
|
| 10 |
DatabaseQueryError,
|
| 11 |
DatabaseQueryRequest,
|
| 12 |
DatabaseQueryResponse,
|
|
|
|
|
|
|
| 13 |
StatementResultSchema,
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
_logger = get_logger(__name__)
|
|
@@ -25,6 +28,81 @@ def _root_cause(exc: Exception) -> str:
|
|
| 25 |
|
| 26 |
class DatabaseService:
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
async def execute_query(self, request: DatabaseQueryRequest) -> DatabaseQueryResponse:
|
| 29 |
start_time = time.monotonic()
|
| 30 |
config = ConnectionConfig(**request.to_connection_config())
|
|
|
|
| 10 |
DatabaseQueryError,
|
| 11 |
DatabaseQueryRequest,
|
| 12 |
DatabaseQueryResponse,
|
| 13 |
+
DatabaseValidateRequest,
|
| 14 |
+
DatabaseValidateResponse,
|
| 15 |
StatementResultSchema,
|
| 16 |
+
TableValidationResult,
|
| 17 |
)
|
| 18 |
|
| 19 |
_logger = get_logger(__name__)
|
|
|
|
| 28 |
|
| 29 |
class DatabaseService:
|
| 30 |
|
| 31 |
+
async def validate_connection(self, request: DatabaseValidateRequest) -> DatabaseValidateResponse:
|
| 32 |
+
start_time = time.monotonic()
|
| 33 |
+
config = ConnectionConfig(**request.to_connection_config())
|
| 34 |
+
tables = list(request.table_or_collection_names)
|
| 35 |
+
results: list[TableValidationResult] = []
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
executor = await pool_manager.get_executor(config)
|
| 39 |
+
except Exception as exc:
|
| 40 |
+
elapsed = round((time.monotonic() - start_time) * 1000, 2)
|
| 41 |
+
_logger.error("Connection failed for %s: %s", config.safe_repr, exc)
|
| 42 |
+
return DatabaseValidateResponse(
|
| 43 |
+
success=False,
|
| 44 |
+
time_ms=elapsed,
|
| 45 |
+
message="Connection failed",
|
| 46 |
+
connection_details=config.safe_repr,
|
| 47 |
+
error_message=_root_cause(exc),
|
| 48 |
+
tables=[],
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
if request.db_type == "mongodb":
|
| 53 |
+
pool = await executor._get_or_create_pool()
|
| 54 |
+
db = pool[config.database]
|
| 55 |
+
await db.command("ping")
|
| 56 |
+
message = "Connected successfully"
|
| 57 |
+
if tables:
|
| 58 |
+
existing = await db.list_collection_names()
|
| 59 |
+
existing_set = set(existing)
|
| 60 |
+
for name in tables:
|
| 61 |
+
results.append(TableValidationResult(
|
| 62 |
+
name=name,
|
| 63 |
+
exists=name in existing_set,
|
| 64 |
+
))
|
| 65 |
+
else:
|
| 66 |
+
test = await executor.execute(["SELECT 1 AS test"])
|
| 67 |
+
if not test or not test[0].success:
|
| 68 |
+
raise RuntimeError(test[0].error if test else "No response")
|
| 69 |
+
message = "Connected successfully"
|
| 70 |
+
if tables:
|
| 71 |
+
for name in tables:
|
| 72 |
+
if request.db_type == "postgresql":
|
| 73 |
+
q = (
|
| 74 |
+
f"SELECT 1 FROM information_schema.tables "
|
| 75 |
+
f"WHERE table_schema = '{config.selected_schema}' AND table_name = '{name}'"
|
| 76 |
+
)
|
| 77 |
+
else:
|
| 78 |
+
q = (
|
| 79 |
+
f"SELECT 1 FROM information_schema.tables "
|
| 80 |
+
f"WHERE TABLE_SCHEMA = DATABASE() AND table_name = '{name}'"
|
| 81 |
+
)
|
| 82 |
+
r = await executor.execute([q])
|
| 83 |
+
exists = bool(r and r[0].success and r[0].rows > 0)
|
| 84 |
+
results.append(TableValidationResult(name=name, exists=exists))
|
| 85 |
+
except Exception as exc:
|
| 86 |
+
elapsed = round((time.monotonic() - start_time) * 1000, 2)
|
| 87 |
+
_logger.error("Validation failed for %s: %s", config.safe_repr, exc)
|
| 88 |
+
return DatabaseValidateResponse(
|
| 89 |
+
success=False,
|
| 90 |
+
time_ms=elapsed,
|
| 91 |
+
message="Validation failed",
|
| 92 |
+
connection_details=config.safe_repr,
|
| 93 |
+
error_message=_root_cause(exc),
|
| 94 |
+
tables=results,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
elapsed = round((time.monotonic() - start_time) * 1000, 2)
|
| 98 |
+
return DatabaseValidateResponse(
|
| 99 |
+
success=True,
|
| 100 |
+
time_ms=elapsed,
|
| 101 |
+
message=message,
|
| 102 |
+
connection_details=config.safe_repr,
|
| 103 |
+
tables=results,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
async def execute_query(self, request: DatabaseQueryRequest) -> DatabaseQueryResponse:
|
| 107 |
start_time = time.monotonic()
|
| 108 |
config = ConnectionConfig(**request.to_connection_config())
|