diff --git a/src/DataService.php b/src/DataService.php index 642203b..8f68322 100644 --- a/src/DataService.php +++ b/src/DataService.php @@ -129,13 +129,11 @@ class DataService } } - // ✅ Метод для форматирования ошибок private function formatPDOError(\PDOException $e, string $schema, string $table, array $params = []): string { $code = $e->getCode(); $message = $e->getMessage(); - // Foreign Key constraint if ($code === '23000' && str_contains($message, 'foreign key constraint')) { if (preg_match('/FOREIGN KEY $$`([^`]+)`$$/', $message, $matches)) { $field = $matches[1]; @@ -144,7 +142,6 @@ class DataService return "Ошибка: нарушена связь с другой таблицей. Проверьте правильность заполнения полей-ссылок."; } - // Duplicate key if ($code === '23000' && str_contains($message, 'Duplicate entry')) { if (preg_match('/Duplicate entry \'([^\']+)\' for key \'([^\']+)\'/', $message, $matches)) { $value = $matches[1]; @@ -154,7 +151,6 @@ class DataService return "Ошибка: запись с таким значением уже существует."; } - // NOT NULL constraint 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]; @@ -239,138 +235,176 @@ class DataService return ['deleted' => $stmt->rowCount()]; } -public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array -{ - error_log("=== НАЧАЛО ИМПОРТА CSV ==="); - error_log("Schema: $schema, Table: $table"); - error_log("Количество строк для импорта: " . count($rows)); - error_log("Структура первой строки: " . json_encode($rows[0] ?? [])); - - if (empty($rows)) { - return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided']; - } - - $inserted = 0; - $errors = 0; - $errorMessages = []; - - // ✅ Собираем информацию о всех столбцах таблицы - $validColumns = []; - foreach ($columns as $c) { - $name = $c['COLUMN_NAME']; - $extra = $c['EXTRA'] ?? ''; + public function insertMultipleRows(string $schema, string $table, array $rows, array $columns): array + { + error_log("=== НАЧАЛО ИМПОРТА CSV ==="); + error_log("Schema: $schema, Table: $table"); + error_log("Количество строк для импорта: " . count($rows)); - error_log("Столбец: $name, Extra: $extra, Nullable: " . ($c['IS_NULLABLE'] ? 'YES' : 'NO') . ", Default: " . ($c['COLUMN_DEFAULT'] ?? 'NULL')); - - // Пропускаем только auto_increment поля - if (!str_contains($extra, 'auto_increment')) { - $validColumns[$name] = [ - 'nullable' => $c['IS_NULLABLE'] ?? false, - 'has_default' => !empty($c['COLUMN_DEFAULT']) || $c['COLUMN_DEFAULT'] === '0', - 'default' => $c['COLUMN_DEFAULT'] ?? null - ]; - } else { - error_log("Пропускаем auto_increment столбец: $name"); + if (empty($rows)) { + return ['inserted' => 0, 'errors' => 0, 'message' => 'No rows provided']; } - } - - error_log("Валидные столбцы для импорта: " . json_encode(array_keys($validColumns))); - $this->pdo->beginTransaction(); + $inserted = 0; + $errors = 0; + $errorMessages = []; - try { - foreach ($rows as $index => $row) { - error_log("\n--- Обработка строки " . ($index + 1) . " ---"); - error_log("Данные строки: " . json_encode($row)); + // ✅ Собираем информацию о всех столбцах таблицы + $validColumns = []; + foreach ($columns as $c) { + $name = $c['COLUMN_NAME']; + $extra = $c['EXTRA'] ?? ''; + $dataType = strtolower($c['DATA_TYPE'] ?? ''); - try { - $insertCols = []; - $placeholders = []; - $params = []; - - // ✅ Перебираем ВСЕ столбцы таблицы (кроме auto_increment) - foreach ($validColumns as $name => $info) { - $value = null; - - // Если столбец есть в CSV строке - if (array_key_exists($name, $row)) { - $value = $row[$name]; - error_log(" Столбец '$name' найден в CSV: '$value'"); - - // ✅ Обрабатываем "NULL" как NULL - if (in_array($value, ['NULL', 'null', ''], true)) { - error_log(" Преобразуем '$value' в NULL"); - $value = null; - } - } else { - error_log(" Столбец '$name' НЕ найден в CSV"); - // ✅ Столбца нет в CSV - устанавливаем NULL или пропускаем - // Если поле имеет значение по умолчанию - пропускаем (БД сама подставит) - if ($info['has_default']) { - error_log(" Пропускаем (есть default): " . $info['default']); - continue; - } - // Если поле nullable - ставим NULL - if ($info['nullable']) { - error_log(" Устанавливаем NULL (nullable)"); - $value = null; - } else { - // Поле обязательное и нет дефолта - ошибка - error_log(" ОШИБКА: обязательное поле без значения"); - throw new \PDOException("Missing required field: $name"); - } - } - - $insertCols[] = "`$name`"; - $placeholders[] = ":$name"; - $params[":$name"] = $value; - error_log(" Добавлено в INSERT: '$name' = " . ($value === null ? 'NULL' : "'$value'")); - } - - if (empty($insertCols)) { - error_log("Нет столбцов для вставки, пропускаем строку"); - continue; - } - - $sql = sprintf( - "INSERT INTO `%s`.`%s` (%s) VALUES (%s)", - $schema, $table, - implode(',', $insertCols), - implode(',', $placeholders) - ); - - error_log("SQL: $sql"); - error_log("Параметры: " . json_encode($params)); - - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - $inserted++; - error_log("✓ Строка успешно вставлена, ID: " . $this->pdo->lastInsertId()); - } catch (\PDOException $e) { - $errors++; - $errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table); - error_log("✗ ОШИБКА при вставке строки " . ($index + 1) . ": " . $e->getMessage()); - error_log(" SQL Code: " . $e->getCode()); - error_log(" SQL State: " . ($e->errorInfo[0] ?? 'unknown')); - $errorMessages[] = $errorMsg; + // ✅ Правильная проверка IS_NULLABLE + $isNullable = ($c['IS_NULLABLE'] === true || $c['IS_NULLABLE'] === 'YES'); + + error_log("Столбец: $name, Type: $dataType, Extra: $extra, Nullable: " . ($isNullable ? 'YES' : 'NO') . ", Default: " . ($c['COLUMN_DEFAULT'] ?? 'NULL')); + + // Пропускаем только auto_increment поля + if (!str_contains($extra, 'auto_increment')) { + $validColumns[$name] = [ + 'nullable' => $isNullable, + 'has_default' => !empty($c['COLUMN_DEFAULT']) || $c['COLUMN_DEFAULT'] === '0', + 'default' => $c['COLUMN_DEFAULT'] ?? null, + 'data_type' => $dataType + ]; + } else { + error_log("Пропускаем auto_increment столбец: $name"); } } + + error_log("Валидные столбцы для импорта: " . json_encode(array_keys($validColumns))); - $this->pdo->commit(); - error_log("=== ИМПОРТ ЗАВЕРШЁН: вставлено=$inserted, ошибок=$errors ==="); - } catch (\Exception $e) { - $this->pdo->rollBack(); - error_log("=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА: " . $e->getMessage() . " ==="); - throw new \RuntimeException('Import failed: ' . $e->getMessage()); + $this->pdo->beginTransaction(); + + try { + foreach ($rows as $index => $row) { + error_log("\n--- Обработка строки " . ($index + 1) . " ---"); + error_log("Данные строки: " . json_encode($row)); + + try { + $insertCols = []; + $placeholders = []; + $params = []; + + // ✅ Перебираем ВСЕ столбцы таблицы (кроме auto_increment) + foreach ($validColumns as $name => $info) { + $value = null; + + // Если столбец есть в CSV строке + if (array_key_exists($name, $row)) { + $value = $row[$name]; + error_log(" Столбец '$name' найден в CSV: '" . var_export($value, true) . "'"); + + // ✅ Обрабатываем пустые значения как NULL + if ($value === null || $value === '' || in_array($value, ['NULL', 'null'], true)) { + error_log(" Преобразуем '$value' в NULL"); + $value = null; + } else { + // ✅ Конвертируем форматы дат для date/datetime полей + if (in_array($info['data_type'], ['date', 'datetime', 'timestamp'])) { + $value = $this->convertDateFormat($value); + error_log(" Конвертация даты: результат = '$value'"); + } + } + } else { + error_log(" Столбец '$name' НЕ найден в CSV"); + // ✅ Столбца нет в CSV - устанавливаем NULL или пропускаем + // Если поле имеет значение по умолчанию - пропускаем (БД сама подставит) + if ($info['has_default']) { + error_log(" Пропускаем (есть default): " . $info['default']); + continue; + } + // Если поле nullable - ставим NULL + if ($info['nullable']) { + error_log(" Устанавливаем NULL (nullable)"); + $value = null; + } else { + // Поле обязательное и нет дефолта - ошибка + error_log(" ОШИБКА: обязательное поле без значения (nullable={$info['nullable']})"); + throw new \PDOException("Missing required field: $name"); + } + } + + $insertCols[] = "`$name`"; + $placeholders[] = ":$name"; + $params[":$name"] = $value; + error_log(" Добавлено в INSERT: '$name' = " . ($value === null ? 'NULL' : "'$value'")); + } + + if (empty($insertCols)) { + error_log("Нет столбцов для вставки, пропускаем строку"); + continue; + } + + $sql = sprintf( + "INSERT INTO `%s`.`%s` (%s) VALUES (%s)", + $schema, $table, + implode(',', $insertCols), + implode(',', $placeholders) + ); + + error_log("SQL: $sql"); + error_log("Параметры: " . json_encode($params)); + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $inserted++; + error_log("✓ Строка успешно вставлена, ID: " . $this->pdo->lastInsertId()); + } catch (\PDOException $e) { + $errors++; + $errorMsg = "Строка " . ($index + 1) . ": " . $this->formatPDOError($e, $schema, $table); + error_log("✗ ОШИБКА при вставке строки " . ($index + 1) . ": " . $e->getMessage()); + error_log(" SQL Code: " . $e->getCode()); + $errorMessages[] = $errorMsg; + } + } + + $this->pdo->commit(); + error_log("=== ИМПОРТ ЗАВЕРШЁН: вставлено=$inserted, ошибок=$errors ==="); + } catch (\Exception $e) { + $this->pdo->rollBack(); + error_log("=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА: " . $e->getMessage() . " ==="); + throw new \RuntimeException('Import failed: ' . $e->getMessage()); + } + + return [ + 'inserted' => $inserted, + 'errors' => $errors, + 'errorMessages' => $errorMessages + ]; } - return [ - 'inserted' => $inserted, - 'errors' => $errors, - 'errorMessages' => $errorMessages - ]; -} - + /** + * Конвертирует различные форматы дат в формат MySQL (YYYY-MM-DD) + */ + private function convertDateFormat(string $date): string + { + $date = trim($date); + + // Уже в нужном формате YYYY-MM-DD + if (preg_match('/^\d{4}-\d{2}-\d{2}/', $date)) { + return substr($date, 0, 10); + } + + // Формат DD.MM.YYYY или DD/MM/YYYY или DD-MM-YYYY + if (preg_match('/^(\d{1,2})[\.\/-](\d{1,2})[\.\/-](\d{4})/', $date, $matches)) { + $day = str_pad($matches[1], 2, '0', STR_PAD_LEFT); + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $year = $matches[3]; + return "$year-$month-$day"; + } + + // Пробуем распарсить через strtotime + $timestamp = strtotime($date); + if ($timestamp !== false) { + return date('Y-m-d', $timestamp); + } + + // Если не удалось распарсить - возвращаем как есть (будет ошибка от БД) + return $date; + } public function exportCSV( string $schema, diff --git a/src/MetaService.php b/src/MetaService.php index c312eac..cee2976 100644 --- a/src/MetaService.php +++ b/src/MetaService.php @@ -79,25 +79,32 @@ class MetaService $enrichedCols = array_map(function($c) use ($fkMap) { $name = $c['COLUMN_NAME']; + // ✅ Явно конвертируем IS_NULLABLE в boolean + $isNullable = ($c['IS_NULLABLE'] === 'YES'); + + // ✅ Проверка наличия дефолтного значения + $hasDefault = ($c['COLUMN_DEFAULT'] !== null) || + str_contains(strtoupper($c['EXTRA'] ?? ''), 'DEFAULT_GENERATED'); + return [ 'COLUMN_NAME' => $name, 'DATA_TYPE' => $c['DATA_TYPE'], 'COLUMN_TYPE' => $c['COLUMN_TYPE'], 'COLUMN_KEY' => $c['COLUMN_KEY'], - 'IS_NULLABLE' => $c['IS_NULLABLE'] === 'YES', + 'IS_NULLABLE' => $isNullable, // ✅ Boolean, не строка 'COLUMN_DEFAULT' => $c['COLUMN_DEFAULT'], - 'HAS_DEFAULT' => !empty($c['COLUMN_DEFAULT']), + 'HAS_DEFAULT' => $hasDefault, 'EXTRA' => $c['EXTRA'], 'IS_AUTO_INCREMENT' => str_contains($c['EXTRA'] ?? '', 'auto_increment'), 'ORDINAL_POSITION' => (int)$c['ORDINAL_POSITION'], - 'IS_REQUIRED' => $c['IS_NULLABLE'] === 'NO' && empty($c['COLUMN_DEFAULT']), + 'IS_REQUIRED' => !$isNullable && !$hasDefault && !str_contains($c['EXTRA'] ?? '', 'auto_increment'), // ✅ Правильная логика 'EDITOR_TYPE' => $this->getEditorType($c), - // ✅ Добавляем информацию о FK 'IS_FOREIGN_KEY' => isset($fkMap[$name]), 'FOREIGN_KEY' => $fkMap[$name] ?? null ]; }, $cols); + return [ 'columns' => $enrichedCols, 'primaryKey' => $pk,