light-infer-chat commited on
Commit
55ae875
·
1 Parent(s): 7d1ad3f
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())