From 01ddb81aa983a6ca4538a62b3a2cc581fcc29ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Tue, 28 Apr 2026 20:19:56 +0300 Subject: [PATCH] feat: implement database management API with MySQL driver and schema operations --- .../app/Contracts/DatabaseDriverInterface.php | 10 + .../Http/Controllers/Api/SchemaController.php | 27 +++ backend/app/Services/Database/MySqlDriver.php | 35 ++++ backend/app/Services/DatabaseService.php | 16 ++ backend/routes/api.php | 2 + frontend/src/components/Sidebar.tsx | 193 +++++++++++++++++- frontend/src/services/api.ts | 2 + 7 files changed, 276 insertions(+), 9 deletions(-) diff --git a/backend/app/Contracts/DatabaseDriverInterface.php b/backend/app/Contracts/DatabaseDriverInterface.php index 03f6896..9027253 100644 --- a/backend/app/Contracts/DatabaseDriverInterface.php +++ b/backend/app/Contracts/DatabaseDriverInterface.php @@ -83,4 +83,14 @@ interface DatabaseDriverInterface * Create a new database. */ public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool; + + /** + * Drop a database. + */ + public function dropDatabase(string $name): bool; + + /** + * Rename a database. + */ + public function renameDatabase(string $oldName, string $newName): bool; } diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php index 9a7245d..03692ea 100644 --- a/backend/app/Http/Controllers/Api/SchemaController.php +++ b/backend/app/Http/Controllers/Api/SchemaController.php @@ -287,4 +287,31 @@ class SchemaController extends Controller return Response::json(['error' => $e->getMessage()], 400); } } + + public function dropDatabase(Request $request, $database) + { + try { + $this->initializeDriver($request); + $this->databaseService->dropDatabase($database); + return Response::json(['message' => "Database '{$database}' dropped successfully"]); + } catch (\Exception $e) { + return Response::json(['error' => $e->getMessage()], 400); + } + } + + public function renameDatabase(Request $request, $database) + { + $request->validate([ + 'newName' => 'required|string|max:64', + ]); + + try { + $this->initializeDriver($request); + $newName = $request->get('newName'); + $this->databaseService->renameDatabase($database, $newName); + return Response::json(['message' => "Database '{$database}' renamed to '{$newName}' successfully"]); + } catch (\Exception $e) { + return Response::json(['error' => $e->getMessage()], 400); + } + } } diff --git a/backend/app/Services/Database/MySqlDriver.php b/backend/app/Services/Database/MySqlDriver.php index ed84f9a..3d83d78 100644 --- a/backend/app/Services/Database/MySqlDriver.php +++ b/backend/app/Services/Database/MySqlDriver.php @@ -454,4 +454,39 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface DB::connection($this->connectionName)->statement("CREATE DATABASE `{$name}` CHARACTER SET {$charset} COLLATE {$collation}"); return true; } + + public function dropDatabase(string $name): bool + { + DB::connection($this->connectionName)->statement("DROP DATABASE `{$name}`"); + return true; + } + + public function renameDatabase(string $oldName, string $newName): bool + { + // 1. Create new database + $this->createDatabase($newName); + + // 2. Get tables from old database + $tables = $this->query("SHOW TABLES FROM `{$oldName}`"); + $key = "Tables_in_{$oldName}"; + + if (!empty($tables)) { + $renameQueries = []; + foreach ($tables as $table) { + $tableName = $table->$key; + $renameQueries[] = "`{$oldName}`.`{$tableName}` TO `{$newName}`.`{$tableName}`"; + } + + // 3. Rename all tables + if (!empty($renameQueries)) { + $sql = "RENAME TABLE " . implode(', ', $renameQueries); + DB::connection($this->connectionName)->statement($sql); + } + } + + // 4. Drop old database + $this->dropDatabase($oldName); + + return true; + } } diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php index 77f0019..c227a61 100644 --- a/backend/app/Services/DatabaseService.php +++ b/backend/app/Services/DatabaseService.php @@ -172,4 +172,20 @@ class DatabaseService { return $this->getDriver()->createDatabase($name, $charset, $collation); } + + /** + * Drop a database. + */ + public function dropDatabase(string $name): bool + { + return $this->getDriver()->dropDatabase($name); + } + + /** + * Rename a database. + */ + public function renameDatabase(string $oldName, string $newName): bool + { + return $this->getDriver()->renameDatabase($oldName, $newName); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index 7a790c6..62cd832 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -11,6 +11,8 @@ Route::get('/user', function (Request $request) { Route::prefix('schema')->group(function () { Route::get('/databases', [SchemaController::class, 'databases']); Route::post('/databases', [SchemaController::class, 'createDatabase']); + Route::delete('/databases/{database}', [SchemaController::class, 'dropDatabase']); + Route::post('/databases/{database}/rename', [SchemaController::class, 'renameDatabase']); Route::get('/tables/{database}', [SchemaController::class, 'tables']); Route::get('/metadata/{database}', [SchemaController::class, 'metadata']); Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 96ec1a7..6d91408 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle } from '@mui/material'; -import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add } from '@mui/icons-material'; +import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle, Menu, MenuItem } from '@mui/material'; +import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add, MoreVert, Delete, Edit } from '@mui/icons-material'; import { useAppStore } from '../store/useAppStore'; import { SchemaService } from '../services/api'; import type { MainNotification } from '../types/database'; @@ -19,7 +19,19 @@ const Sidebar: React.FC = () => { const [openNewDbDialog, setOpenNewDbDialog] = useState(false); const [newDbName, setNewDbName] = useState(''); - const [isCreatingDb, setIsCreatingDb] = useState(false); + + const [openRenameDialog, setOpenRenameDialog] = useState(false); + const [dbToRename, setDbToRename] = useState(''); + const [renamedDbName, setRenamedDbName] = useState(''); + + const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false); + const [dbToDelete, setDbToDelete] = useState(''); + + const [isProcessing, setIsProcessing] = useState(false); + + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [selectedDbForMenu, setSelectedDbForMenu] = useState(''); + const [notification, setNotification] = useState({ open: false, message: '', @@ -102,7 +114,7 @@ const Sidebar: React.FC = () => { const handleCreateDatabase = async () => { if (!newDbName.trim()) return; - setIsCreatingDb(true); + setIsProcessing(true); try { await SchemaService.createDatabase(newDbName); setNotification({ @@ -122,10 +134,95 @@ const Sidebar: React.FC = () => { severity: 'error' }); } finally { - setIsCreatingDb(false); + setIsProcessing(false); } }; + const handleRenameDatabase = async () => { + if (!renamedDbName.trim() || !dbToRename) return; + + setIsProcessing(true); + try { + await SchemaService.renameDatabase(dbToRename, renamedDbName); + setNotification({ + open: true, + title: 'Success', + message: `Database '${dbToRename}' renamed to '${renamedDbName}' successfully.`, + severity: 'success' + }); + if (activeDatabase === dbToRename) { + setActiveDatabase(renamedDbName); + } + setOpenRenameDialog(false); + setDbToRename(''); + setRenamedDbName(''); + fetchDatabases(); + } catch (error: any) { + setNotification({ + open: true, + title: 'Error', + message: error.response?.data?.error || error.message, + severity: 'error' + }); + } finally { + setIsProcessing(false); + } + }; + + const handleDeleteDatabase = async () => { + if (!dbToDelete) return; + + setIsProcessing(true); + try { + await SchemaService.dropDatabase(dbToDelete); + setNotification({ + open: true, + title: 'Success', + message: `Database '${dbToDelete}' deleted successfully.`, + severity: 'success' + }); + if (activeDatabase === dbToDelete) { + setActiveDatabase(null); + } + setOpenDeleteConfirm(false); + setDbToDelete(''); + fetchDatabases(); + } catch (error: any) { + setNotification({ + open: true, + title: 'Error', + message: error.response?.data?.error || error.message, + severity: 'error' + }); + } finally { + setIsProcessing(false); + } + }; + + const handleMenuOpen = (event: React.MouseEvent, db: string) => { + event.stopPropagation(); + setMenuAnchorEl(event.currentTarget); + setSelectedDbForMenu(db); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + setSelectedDbForMenu(''); + }; + + const handleMenuRename = () => { + setDbToRename(selectedDbForMenu); + setRenamedDbName(selectedDbForMenu); + setOpenRenameDialog(true); + handleMenuClose(); + }; + + const handleMenuDelete = () => { + setDbToDelete(selectedDbForMenu); + setOpenDeleteConfirm(true); + handleMenuClose(); + }; + return ( @@ -186,6 +283,9 @@ const Sidebar: React.FC = () => { {db}} /> + handleMenuOpen(e, db)} sx={{ opacity: 0, '&:hover': { opacity: 1 }, '.MuiListItemButton-root:hover &': { opacity: 0.5 } }}> + + @@ -290,24 +390,99 @@ const Sidebar: React.FC = () => { variant="outlined" value={newDbName} onChange={(e) => setNewDbName(e.target.value)} - disabled={isCreatingDb} + disabled={isProcessing} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateDatabase(); }} /> - + + {/* Rename Database Dialog */} + setOpenRenameDialog(false)} maxWidth="xs" fullWidth> + Rename Database + + + Renaming database {dbToRename}. + This will create a new database and move all tables. + + setRenamedDbName(e.target.value)} + disabled={isProcessing} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRenameDatabase(); + }} + /> + + + + + + + + {/* Delete Database Confirm */} + setOpenDeleteConfirm(false)}> + Delete Database? + + + Are you sure you want to delete database {dbToDelete}? + This action cannot be undone and all data will be lost. + + + + + + + + + {/* Database Item Menu */} + + + + Rename + + + + Delete + + + {/* Local Notification */} api.get('/schema/databases'), createDatabase: (name: string, charset?: string, collation?: string) => api.post('/schema/databases', { name, charset, collation }), + dropDatabase: (db: string) => api.delete(`/schema/databases/${db}`, { params: { database: db } }), + renameDatabase: (db: string, newName: string) => api.post(`/schema/databases/${db}/rename`, { newName }, { params: { database: db } }), getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }), getDatabaseMetadata: (db: string) => api.get(`/schema/metadata/${db}`, { params: { database: db } }), getTablesMetadata: (db: string) => api.get(`/schema/metadata/${db}/tables`, { params: { database: db } }),