make(Kernel::class)->bootstrap(); $connection = DB::connection(); if ($connection->getDriverName() !== 'pgsql' || $connection->getDatabaseName() !== 'compras_dev' || ! $app->environment('local')) { throw new LogicException('Aplicação recusada fora de compras_dev/local.'); } $pdo = $connection->getPdo(); $migration = '2026_10_28_000010_create_invoices'; if ($pdo->query("SELECT count(*) FROM migrations WHERE migration = '$migration'")->fetchColumn() > 0) { throw new LogicException('Migração já aplicada; não repetir.'); } $config = $connection->getConfig(); $bin = 'C:/Program Files/PostgreSQL/17/bin/'; foreach (['pg_dump.exe', 'pg_restore.exe'] as $tool) { if (! is_file($bin.$tool)) { throw new LogicException('Ferramentas PostgreSQL 17 ausentes.'); } } $envHash = hash_file('sha256', base_path('.env')); $directory = storage_path('app/private/backups/invoice-000010-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3))); if (! mkdir($directory, 0700, true)) { throw new RuntimeException('Não foi possível criar backup privado.'); } $env = ['PGHOST' => $config['host'], 'PGPORT' => (string) $config['port'], 'PGUSER' => $config['username'], 'PGPASSWORD' => $config['password'], 'PGSSLMODE' => $config['sslmode'] ?? 'prefer']; $quote = fn (string $name) => '"'.str_replace('"', '""', $name).'"'; $fingerprints = function (PDO $db, array $tables) use ($quote): array { $result = []; foreach ($tables as $table) { $rows = $db->query('SELECT md5(row_to_json(t)::text) FROM public.'.$quote($table).' t')->fetchAll(PDO::FETCH_COLUMN); sort($rows, SORT_STRING); $result[$table] = ['count' => count($rows), 'sha256' => hash('sha256', implode('\n', $rows))]; } return $result; }; $tables = $pdo->query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename")->fetchAll(PDO::FETCH_COLUMN); $restoreDatabase = 'compras_restore_check_'.bin2hex(random_bytes(6)); $restoreCreated = false; try { // pg_dump and the manifest read exactly the same committed snapshot. $pdo->beginTransaction(); $pdo->exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY'); $snapshot = $pdo->query('SELECT pg_export_snapshot()')->fetchColumn(); $backupState = $fingerprints($pdo, $tables); $dump = new Process([$bin.'pg_dump.exe', '--format=custom', '--no-owner', '--no-acl', '--snapshot='.$snapshot, '--file='.$directory.'/database.dump', '--dbname='.$config['database']], base_path(), $env); $dump->setTimeout(120); $dump->run(); if (! $dump->isSuccessful()) { throw new RuntimeException('Falha no backup PostgreSQL; migração não executada.'); } $pdo->commit(); $pdo->exec('CREATE DATABASE '.$quote($restoreDatabase)); $restoreCreated = true; $restore = new Process([$bin.'pg_restore.exe', '--exit-on-error', '--no-owner', '--no-acl', '--dbname='.$restoreDatabase, $directory.'/database.dump'], base_path(), $env); $restore->setTimeout(120); $restore->run(); if (! $restore->isSuccessful()) { throw new RuntimeException('Restauro de verificação falhou; migração não executada.'); } config(['database.connections.budget_restore' => [...$config, 'url' => null, 'database' => $restoreDatabase]]); $restored = DB::connection('budget_restore')->getPdo(); if ($fingerprints($restored, $tables) !== $backupState) { throw new RuntimeException('Restauro não corresponde ao backup; migração recusada.'); } file_put_contents($directory.'/manifest.json', json_encode(['tables' => $backupState, 'dump_sha256' => hash_file('sha256', $directory.'/database.dump')], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); file_put_contents($directory.'/RECUPERACAO.txt', "Backup custom PostgreSQL 17 verificado por restauro numa base separada.\nUsar pg_restore --exit-on-error --no-owner --no-acl --dbname=NOVA_BASE database.dump.\nAs credenciais pertencem ao ambiente local e não estão neste manifesto.\nNão restaurar por cima da base existente.\n"); DB::disconnect('budget_restore'); unset($restored); $pdo->exec('DROP DATABASE '.$quote($restoreDatabase)); $restoreCreated = false; $businessTables = array_values(array_diff($tables, ['migrations', 'sessions', 'cache', 'cache_locks', 'jobs', 'failed_jobs', 'job_batches'])); $before = $fingerprints($pdo, $businessTables); $oldMigrations = $pdo->query('SELECT id, migration, batch FROM migrations ORDER BY id')->fetchAll(PDO::FETCH_ASSOC); $constraints = $pdo->query("SELECT conname, pg_get_constraintdef(oid) AS definition FROM pg_constraint WHERE connamespace = 'public'::regnamespace ORDER BY conname, definition")->fetchAll(PDO::FETCH_ASSOC); $exit = Artisan::call('migrate', ['--path' => 'database/migrations/'.$migration.'.php', '--force' => true]); if ($exit !== 0) { throw new RuntimeException('Migração falhou; consultar backup privado.'); } $after = $fingerprints($pdo, $businessTables); $newMigrations = $pdo->query('SELECT id, migration, batch FROM migrations ORDER BY id')->fetchAll(PDO::FETCH_ASSOC); $newConstraints = $pdo->query("SELECT conname, pg_get_constraintdef(oid) AS definition FROM pg_constraint WHERE connamespace = 'public'::regnamespace ORDER BY conname, definition")->fetchAll(PDO::FETCH_ASSOC); if ($before !== $after || array_slice($newMigrations, 0, count($oldMigrations)) !== $oldMigrations || count($newMigrations) !== count($oldMigrations) + 1 || array_diff(array_map('serialize', $constraints), array_map('serialize', $newConstraints)) !== [] || hash_file('sha256', base_path('.env')) !== $envHash) { throw new RuntimeException('Verificação de preservação falhou; não executar mais escritas.'); } $report = ['verified_at_utc' => gmdate('c'), 'migration' => $migration, 'batch' => (int) end($newMigrations)['batch'], 'backup_created' => true, 'backup_bytes' => filesize($directory.'/database.dump'), 'restore_verified' => true, 'restored_tables' => count($tables), 'restore_database_dropped' => true, 'business_records_unchanged' => true, 'existing_constraints_preserved' => true, 'env_unchanged' => true, 'invoices_count' => (int) $pdo->query('SELECT count(*) FROM invoices')->fetchColumn(), 'budget_consumptions_count' => (int) $pdo->query('SELECT count(*) FROM budget_consumptions')->fetchColumn(), 'operational_validation' => false]; file_put_contents(base_path('docs/facturas-migracao-000010.json'), json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL); echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; } finally { if ($pdo->inTransaction()) { $pdo->rollBack(); } DB::disconnect('budget_restore'); if ($restoreCreated && preg_match('/^compras_restore_check_[a-f0-9]{12}$/', $restoreDatabase) && $restoreDatabase !== $connection->getDatabaseName()) { $pdo->exec('DROP DATABASE '.$quote($restoreDatabase)); } }