diff --git a/public/app.js b/public/app.js index e74b283..0b007b6 100644 --- a/public/app.js +++ b/public/app.js @@ -880,26 +880,60 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => }); if (result.errorMessages && result.errorMessages.length > 0) { - const errorsToShow = result.errorMessages.slice(0, 10); - const moreErrors = result.errorMessages.length > 10 - ? `\n... и еще ${result.errorMessages.length - 10} ошибок` - : ''; + // ✅ Группируем ошибки по типам + const fkErrors = result.errorMessages.filter(msg => msg.includes('СВЯЗАННОЕ ЗНАЧЕНИЕ') || msg.includes('ОШИБКА СВЯЗИ')); + const otherErrors = result.errorMessages.filter(msg => !fkErrors.includes(msg)); - let errorDetails = `Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\n`; - errorDetails += 'ПЕРВЫЕ ОШИБКИ:\n'; - errorDetails += '═══════════════════════════════════════\n'; - errorDetails += errorsToShow.join('\n\n'); - errorDetails += moreErrors; + let errorDetails = `📊 РЕЗУЛЬТАТ ИМПОРТА:\n`; + errorDetails += `✅ Импортировано: ${result.inserted}\n`; + errorDetails += `❌ Ошибок: ${result.errors}\n\n`; + + if (fkErrors.length > 0) { + errorDetails += `⚠️ ОШИБКИ СВЯЗАННЫХ КЛЮЧЕЙ (${fkErrors.length}):\n`; + errorDetails += `═══════════════════════════════════════\n`; + errorDetails += `Значения не найдены в справочниках.\n`; + errorDetails += `Создайте их вручную или исправьте CSV.\n\n`; + + // Показываем первые 5 FK ошибок + const fkToShow = fkErrors.slice(0, 5); + errorDetails += fkToShow.join('\n\n'); + + if (fkErrors.length > 5) { + errorDetails += `\n\n... и еще ${fkErrors.length - 5} FK ошибок`; + } + } + + if (otherErrors.length > 0) { + errorDetails += `\n\n📋 ДРУГИЕ ОШИБКИ (${otherErrors.length}):\n`; + errorDetails += `═══════════════════════════════════════\n`; + + const othersToShow = otherErrors.slice(0, 5); + errorDetails += othersToShow.join('\n\n'); + + if (otherErrors.length > 5) { + errorDetails += `\n\n... и еще ${otherErrors.length - 5} ошибок`; + } + } alert(errorDetails); - console.group('📋 Детали импорта CSV'); - console.log(`✅ Импортировано: ${result.inserted}`); + // Детальный лог в консоль + console.group('📋 ДЕТАЛИ ИМПОРТА CSV'); + console.log(`✅ Успешно: ${result.inserted}`); console.log(`❌ Ошибок: ${result.errors}`); - console.log('\nВсе ошибки:'); - result.errorMessages.forEach((msg, idx) => { - console.log(`${idx + 1}. ${msg}`); - }); + + if (fkErrors.length > 0) { + console.group(`⚠️ FK Ошибки (${fkErrors.length})`); + fkErrors.forEach((msg, idx) => console.log(`${idx + 1}. ${msg}`)); + console.groupEnd(); + } + + if (otherErrors.length > 0) { + console.group(`📋 Другие ошибки (${otherErrors.length})`); + otherErrors.forEach((msg, idx) => console.log(`${idx + 1}. ${msg}`)); + console.groupEnd(); + } + console.groupEnd(); } else { alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`); @@ -913,6 +947,74 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => e.target.value = ''; } }); +document.getElementById('btnAnalyzeCSV').addEventListener('click', () => { + if (!currentSchema || !currentTable) { + alert('Сначала выберите таблицу'); + return; + } + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.csv'; + + input.addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + + try { + const text = await file.text(); + const rows = parseCSV(text); + + if (rows.length === 0) { + alert('CSV файл пуст'); + return; + } + + const headers = rows[0]; + const dataRows = rows.slice(1); + + // Анализируем уникальные значения FK полей + const fkFields = currentMeta.columns.filter(c => c.IS_FOREIGN_KEY); + + if (fkFields.length === 0) { + alert('В таблице нет полей со связями (FK)'); + return; + } + + let analysis = '🔍 АНАЛИЗ СВЯЗАННЫХ ПОЛЕЙ:\n\n'; + + for (const fk of fkFields) { + const colIndex = headers.indexOf(fk.COLUMN_NAME); + if (colIndex === -1) continue; + + const uniqueValues = new Set(); + dataRows.forEach(row => { + const val = row[colIndex]; + if (val && val !== 'NULL') { + uniqueValues.add(val); + } + }); + + analysis += `📌 ${fk.COLUMN_NAME} → ${fk.FOREIGN_KEY.ref_table}.${fk.FOREIGN_KEY.ref_column}\n`; + analysis += ` Уникальных значений: ${uniqueValues.size}\n`; + analysis += ` Значения: ${Array.from(uniqueValues).slice(0, 10).join(', ')}`; + if (uniqueValues.size > 10) { + analysis += `, ... (еще ${uniqueValues.size - 10})`; + } + analysis += '\n\n'; + } + + analysis += '\n⚠️ Убедитесь что эти значения существуют в связанных таблицах!'; + + alert(analysis); + + } catch (err) { + alert('Ошибка анализа: ' + err.message); + } + }); + + input.click(); +}); document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { diff --git a/public/index.html b/public/index.html index 752d6c8..19de8b2 100644 --- a/public/index.html +++ b/public/index.html @@ -184,6 +184,7 @@ +
diff --git a/src/DataService.php b/src/DataService.php index 5021f8f..7ee3576 100644 --- a/src/DataService.php +++ b/src/DataService.php @@ -427,6 +427,9 @@ public function insertMultipleRows(string $schema, string $table, array $rows, a ]; } +/** + * Форматирует ошибку импорта с детальной информацией + */ /** * Форматирует ошибку импорта с детальной информацией */ @@ -444,10 +447,9 @@ private function formatImportError(\PDOException $e, int $lineNumber, array $row $value = $rowData[$field] ?? 'N/A'; $columnType = $columnTypes[$field]['type'] ?? 'unknown'; - // Проверяем, не слишком ли большое число if (is_numeric($value)) { $valueLength = strlen((string)$value); - return $baseMsg . "Поле '{$field}' ({$columnType}): значение '{$value}' (длина: {$valueLength}) слишком большое или имеет неверный формат. " . + return $baseMsg . "Поле '{$field}' ({$columnType}): значение '{$value}' (длина: {$valueLength}) слишком большое. " . "Данные строки: " . $this->formatRowData($rowData); } @@ -457,15 +459,44 @@ private function formatImportError(\PDOException $e, int $lineNumber, array $row return $baseMsg . "Данные обрезаны. Строка: " . $this->formatRowData($rowData); } - // Foreign Key constraint + // ✅ Foreign Key constraint - УЛУЧШЕННАЯ ДИАГНОСТИКА if ($code === '23000' && str_contains($message, 'foreign key constraint')) { + // Пытаемся извлечь имя FK constraint + if (preg_match('/CONSTRAINT `([^`]+)`/', $message, $matches)) { + $constraintName = $matches[1]; + + // Из имени constraint часто можно понять поле (например: fk_table_field) + // Пробуем найти поле в rowData которое может быть причиной + $suspectFields = []; + foreach ($rowData as $fieldName => $fieldValue) { + if ($fieldValue !== null && $fieldValue !== '') { + // Если имя constraint содержит имя поля + if (stripos($constraintName, $fieldName) !== false) { + $suspectFields[] = "{$fieldName}='{$fieldValue}'"; + } + } + } + + if (!empty($suspectFields)) { + return $baseMsg . "❌ СВЯЗАННОЕ ЗНАЧЕНИЕ НЕ НАЙДЕНО: " . implode(', ', $suspectFields) . + ". Добавьте это значение в связанную таблицу или исправьте в CSV. " . + "Constraint: {$constraintName}"; + } + + return $baseMsg . "❌ ОШИБКА СВЯЗИ (FK): {$constraintName}. " . + "Проверьте значения полей-ссылок. Данные: " . $this->formatRowData($rowData); + } + + // Старый формат ошибки if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) { $field = $matches[1]; $value = $rowData[$field] ?? 'N/A'; - return $baseMsg . "Поле '{$field}' = '{$value}': значение не существует в связанной таблице. " . - "Данные: " . $this->formatRowData($rowData); + return $baseMsg . "❌ Поле '{$field}' = '{$value}': значение НЕ СУЩЕСТВУЕТ в связанной таблице. " . + "Создайте это значение в справочнике или исправьте CSV."; } - return $baseMsg . "Нарушена связь с другой таблицей. Данные: " . $this->formatRowData($rowData); + + return $baseMsg . "❌ ОШИБКА СВЯЗИ: одно из значений не существует в связанной таблице. " . + "Данные: " . $this->formatRowData($rowData); } // Duplicate key @@ -473,8 +504,7 @@ private function formatImportError(\PDOException $e, int $lineNumber, array $row if (preg_match('/Duplicate entry \'([^\']+)\' for key \'([^\']+)\'/', $message, $matches)) { $value = $matches[1]; $key = $matches[2]; - return $baseMsg . "Дубликат: значение '{$value}' (ключ '{$key}') уже существует. " . - "Данные: " . $this->formatRowData($rowData); + return $baseMsg . "Дубликат: значение '{$value}' (ключ '{$key}') уже существует."; } return $baseMsg . "Дубликат записи. Данные: " . $this->formatRowData($rowData); } @@ -483,14 +513,13 @@ private function formatImportError(\PDOException $e, int $lineNumber, array $row if (str_contains($message, "cannot be null") || str_contains($message, "doesn't have a default value")) { if (preg_match('/Column \'([^\']+)\'/', $message, $matches)) { $field = $matches[1]; - return $baseMsg . "Поле '{$field}' обязательно для заполнения. " . - "Данные: " . $this->formatRowData($rowData); + return $baseMsg . "Поле '{$field}' обязательно для заполнения (не может быть пустым)."; } - return $baseMsg . "Не заполнены обязательные поля. Данные: " . $this->formatRowData($rowData); + return $baseMsg . "Не заполнены обязательные поля."; } // Общая ошибка БД - return $baseMsg . "Ошибка БД: " . $message . ". Данные: " . $this->formatRowData($rowData); + return $baseMsg . "Ошибка БД: " . $message; } /** @@ -544,7 +573,31 @@ private function formatRowData(array $rowData): string // Если не удалось распарсить - возвращаем как есть return $date; } - // ✅ ДОБАВЬТЕ ЭТИ ДВА МЕТОДА СЮДА: + + /** + * Получает информацию о Foreign Keys таблицы + */ + public function getForeignKeyInfo(string $schema, string $table): array + { + $sql = " + SELECT + COLUMN_NAME, + CONSTRAINT_NAME, + REFERENCED_TABLE_SCHEMA, + REFERENCED_TABLE_NAME, + REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = :schema + AND TABLE_NAME = :table + AND REFERENCED_TABLE_NAME IS NOT NULL + "; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute([':schema' => $schema, ':table' => $table]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + private function isNumericType(string $dataType): bool {