diff --git a/public/app.js b/public/app.js index 692f347..6e53baf 100644 --- a/public/app.js +++ b/public/app.js @@ -569,7 +569,7 @@ function escapeHtml(text) { return div.innerHTML; } -// ✅ УДАЛИТЬ +// ✅ УДАЛИТЬ (с реальным прогрессом для больших объемов) document.getElementById('btnDelete').addEventListener('click', async () => { if (!table || !currentSchema || !currentTable) return; @@ -586,40 +586,106 @@ document.getElementById('btnDelete').addEventListener('click', async () => { if (!confirm(confirmMsg)) return; - try { - let deleted = 0; - let errors = 0; + const rowsArray = Array.from(selectedRowsData.values()); + + // Для очень больших удалений - показываем прогресс + const BATCH_SIZE = 1000; // Удаляем по 1000 строк за раз + const batches = []; + for (let i = 0; i < rowsArray.length; i += BATCH_SIZE) { + batches.push(rowsArray.slice(i, i + BATCH_SIZE)); + } + + if (batches.length > 1) { + // Множество батчей - показываем реальный прогресс + const modal = createProgressModal(`Удаление ${count} записей...`); + document.body.appendChild(modal); - // Удаляем все выделенные строки из selectedRowsData - for (const [key, rowData] of selectedRowsData) { - try { - await api('/api/table/delete', 'POST', { + const progressBar = modal.querySelector('.progress-bar'); + const progressText = modal.querySelector('.spinner').parentElement; + + let totalDeleted = 0; + let totalErrors = 0; + let allErrorMessages = []; + + try { + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + + progressText.innerHTML = ` Обработка ${i + 1} из ${batches.length} батчей...`; + + const result = await api('/api/table/delete-batch', 'POST', { schema: currentSchema, table: currentTable, - row: rowData + rows: batch }); - deleted++; - } catch (e) { - console.error(`Ошибка удаления строки ${key}:`, e); - errors++; + + totalDeleted += result.deleted; + totalErrors += result.errors; + if (result.errorMessages) { + allErrorMessages.push(...result.errorMessages); + } + + // Обновляем прогресс + const progress = ((i + 1) / batches.length) * 100; + progressBar.style.width = progress + '%'; } + + document.body.removeChild(modal); + + selectedRowsData.clear(); + updateSelectionCounter(); + await table.replaceData(); + + if (totalErrors > 0) { + const errorsToShow = allErrorMessages.slice(0, 10); + const moreErrors = allErrorMessages.length > 10 + ? `\n... и еще ${allErrorMessages.length - 10} ошибок` + : ''; + alert(`Удалено строк: ${totalDeleted}\nОшибок: ${totalErrors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`); + } else { + alert(`✓ Успешно удалено строк: ${totalDeleted}`); + } + } catch (e) { + document.body.removeChild(modal); + console.error(e); + alert('Ошибка удаления: ' + e.message); } - - selectedRowsData.clear(); - updateSelectionCounter(); - await table.replaceData(); - - if (errors > 0) { - alert(`Удалено строк: ${deleted}\nОшибок: ${errors}`); - } else { - alert(`✓ Удалено строк: ${deleted}`); + } else { + // Один батч - используем простое модальное окно + const modal = createProgressModal('Удаление записей...'); + document.body.appendChild(modal); + + try { + const result = await api('/api/table/delete-batch', 'POST', { + schema: currentSchema, + table: currentTable, + rows: rowsArray + }); + + document.body.removeChild(modal); + + selectedRowsData.clear(); + updateSelectionCounter(); + await table.replaceData(); + + if (result.errors > 0) { + const errorsToShow = result.errorMessages.slice(0, 10); + const moreErrors = result.errorMessages.length > 10 + ? `\n... и еще ${result.errorMessages.length - 10} ошибок` + : ''; + alert(`Удалено строк: ${result.deleted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`); + } else { + alert(`✓ Удалено строк: ${result.deleted}`); + } + } catch (e) { + document.body.removeChild(modal); + console.error(e); + alert('Ошибка удаления: ' + e.message); } - } catch (e) { - console.error(e); - alert('Ошибка удаления: ' + e.message); } }); + // ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ========== function detectDelimiter(text) { diff --git a/public/index.php b/public/index.php index eecde63..adbd460 100644 --- a/public/index.php +++ b/public/index.php @@ -254,4 +254,23 @@ $app->get('/api/fk-values', function (Request $request, Response $response) use $response->getBody()->write(json_encode(['values' => $values])); return $response->withHeader('Content-Type', 'application/json'); }); + +// API: массовое удаление строк (batch delete) +$app->post('/api/table/delete-batch', function (Request $request, Response $response) use ($container) { + $payload = json_decode((string)$request->getBody(), true); + $pdo = $container->get('db'); + $meta = new \App\MetaService($pdo); + $ds = new \App\DataService($pdo); + + $schema = $payload['schema']; + $table = $payload['table']; + $rows = $payload['rows'] ?? []; // Массив строк для удаления + $metaArr = $meta->getTableMeta($schema, $table); + + $result = $ds->deleteMultipleRows($schema, $table, $rows, $metaArr['primaryKey']); + $response->getBody()->write(json_encode($result)); + return $response->withHeader('Content-Type', 'application/json'); +}); + + $app->run(); diff --git a/src/DataService.php b/src/DataService.php index 120f5ff..d5657f3 100644 --- a/src/DataService.php +++ b/src/DataService.php @@ -435,6 +435,115 @@ class DataService ]; } + /** + * Массовое удаление строк (оптимизированное) + */ + public function deleteMultipleRows(string $schema, string $table, array $rows, array $pk): array + { + if (empty($rows)) { + return ['deleted' => 0, 'errors' => 0, 'message' => 'No rows provided']; + } + + if (empty($pk)) { + throw new \RuntimeException('No primary key — delete disabled'); + } + + error_log("=== МАССОВОЕ УДАЛЕНИЕ: Schema=$schema, Table=$table, Строк=" . count($rows) . " ==="); + + $deleted = 0; + $errors = 0; + $errorMessages = []; + + $this->pdo->beginTransaction(); + + try { + // Если PK состоит из одного поля - используем WHERE IN (самый быстрый способ) + if (count($pk) === 1) { + $pkField = $pk[0]; + + // Группируем по батчам (по 500 строк) для избежания превышения лимитов MySQL + $batchSize = 500; + $batches = array_chunk($rows, $batchSize); + + foreach ($batches as $batchIndex => $batch) { + $pkValues = []; + + foreach ($batch as $row) { + if (!array_key_exists($pkField, $row)) { + $errors++; + $errorMessages[] = "Строка не содержит PK поле: $pkField"; + continue; + } + $pkValues[] = $row[$pkField]; + } + + if (empty($pkValues)) { + continue; + } + + // Удаляем батч за один запрос + $placeholders = implode(',', array_fill(0, count($pkValues), '?')); + $sql = "DELETE FROM `{$schema}`.`{$table}` WHERE `{$pkField}` IN ($placeholders)"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($pkValues); + + $deletedInBatch = $stmt->rowCount(); + $deleted += $deletedInBatch; + + error_log("Батч " . ($batchIndex + 1) . ": удалено $deletedInBatch строк"); + } + } else { + // Составной PK - удаляем по одной строке (но в одной транзакции) + foreach ($rows as $index => $row) { + try { + $whereParts = []; + $params = []; + + foreach ($pk as $name) { + if (!array_key_exists($name, $row)) { + throw new \RuntimeException("Missing PK value: $name"); + } + $whereParts[] = "`$name` = :pk_$name"; + $params[":pk_$name"] = $row[$name]; + } + + $sql = sprintf( + "DELETE FROM `%s`.`%s` WHERE %s", + $schema, $table, + implode(' AND ', $whereParts) + ); + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $deleted += $stmt->rowCount(); + } catch (\PDOException $e) { + $errors++; + $errorMessages[] = "Строка " . ($index + 1) . ": " . $e->getMessage(); + + if ($errors <= 10) { + error_log("Ошибка удаления строки " . ($index + 1) . ": " . $e->getMessage()); + } + } + } + } + + $this->pdo->commit(); + error_log("=== УДАЛЕНИЕ ЗАВЕРШЕНО: удалено=$deleted, ошибок=$errors ==="); + } catch (\Exception $e) { + $this->pdo->rollBack(); + error_log("=== КРИТИЧЕСКАЯ ОШИБКА УДАЛЕНИЯ: " . $e->getMessage() . " ==="); + throw new \RuntimeException('Delete failed: ' . $e->getMessage()); + } + + return [ + 'deleted' => $deleted, + 'errors' => $errors, + 'errorMessages' => $errorMessages + ]; + } + + private function arrayToCSV(array $headers, array $rows): string { $output = fopen('php://temp', 'r+');