Skip to content

Commit

Permalink
Merge pull request #130 from fleetbase/dev-v1.5.25
Browse files Browse the repository at this point in the history
DataPurger and SqlDumper improved to include FK relational data
  • Loading branch information
roncodes authored Jan 29, 2025
2 parents 6a43479 + 39ecdbe commit be8d40a
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 36 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.5.24",
"version": "1.5.25",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
121 changes: 103 additions & 18 deletions src/Support/DataPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

class DataPurger
{
/**
* Delete all data related to a company.
* Delete all data related to a company, including foreign key relationships.
*
* @param \Fleetbase\Models\Company $company
* @param bool $deletePermanently whether to permanently delete the company or soft delete
Expand All @@ -27,43 +28,57 @@ public static function deleteCompanyData($company, bool $deletePermanently = tru

DB::setDefaultConnection($connectionName);

// Disable foreign key checks
// Disable foreign key checks for safe deletion
DB::statement('SET FOREIGN_KEY_CHECKS=0;');

try {
// Fetch all table names
$tables = DB::select('SHOW TABLES');

// Track related records for deletion
$relatedRecords = [];

foreach ($tables as $table) {
$tableName = array_values((array) $table)[0]; // Get table name
$columns = Schema::getColumnListing($tableName);

// Check if table has a `company_uuid` column
if (!in_array('company_uuid', $columns)) {
// Skip system tables
if (Str::startsWith($tableName, ['registry_', 'billing_'])) {
continue;
}

// Count rows to be deleted
$rowCount = DB::table($tableName)->where('company_uuid', $companyUuid)->count();

if ($rowCount > 0) {
// Delete rows associated with the company UUID
DB::table($tableName)->where('company_uuid', $companyUuid)->delete();

// Output verbose logs
if ($verbose) {
echo "Deleted {$rowCount} rows from {$tableName} for company_uuid {$companyUuid}.\n";
// Check if table has a `company_uuid` column (direct deletion)
if (in_array('company_uuid', $columns)) {
$rowCount = DB::table($tableName)->where('company_uuid', $companyUuid)->count();

if ($rowCount > 0) {
// Store related record primary keys for cascade deletion
$primaryKey = self::getPrimaryKey($columns);
if ($primaryKey) {
$relatedRecords[$tableName] = DB::table($tableName)
->where('company_uuid', $companyUuid)
->pluck($primaryKey)
->toArray();
}

// Delete the records
DB::table($tableName)->where('company_uuid', $companyUuid)->delete();

if ($verbose) {
echo "Deleted {$rowCount} rows from {$tableName} for company_uuid {$companyUuid}.\n";
}
} elseif ($verbose) {
echo "No rows found in {$tableName} for company_uuid {$companyUuid}.\n";
}
} elseif ($verbose) {
echo "No rows found in {$tableName} for company_uuid {$companyUuid}.\n";
}
}

// Handle dependent records by foreign keys
self::deleteRelatedRecords($relatedRecords, $verbose);
} catch (\Exception $e) {
// Re-enable foreign key checks in case of an error
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
throw $e;
} finally {
// Re-enable foreign key checks after processing
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
}
Expand All @@ -90,4 +105,74 @@ public static function deleteCompanyData($company, bool $deletePermanently = tru
}
}
}

/**
* Deletes records from tables that reference previously deleted records.
*
* @param array $relatedRecords an associative array of table names and their primary keys to delete
* @param bool $verbose whether to output logs
*/
protected static function deleteRelatedRecords(array $relatedRecords, bool $verbose = false)
{
$processedTables = [];

foreach ($relatedRecords as $table => $primaryKeys) {
$columns = Schema::getColumnListing($table);

foreach ($columns as $column) {
foreach (Schema::getAllTables() as $relatedTable) {
$relatedTableName = array_values((array) $relatedTable)[0];

// Skip system tables
if (Str::startsWith($relatedTableName, ['registry_', 'billing_'])) {
continue;
}

if (in_array($relatedTableName, $processedTables)) {
continue; // Skip already processed tables
}

$relatedColumns = Schema::getColumnListing($relatedTableName);
foreach ($relatedColumns as $relatedColumn) {
if (self::isForeignKey($relatedColumn, $table)) {
// Find dependent records
$dependentRecords = DB::table($relatedTableName)
->whereIn($relatedColumn, $primaryKeys)
->pluck(self::getPrimaryKey($relatedColumns))
->toArray();

if (!empty($dependentRecords)) {
DB::table($relatedTableName)->whereIn($relatedColumn, $primaryKeys)->delete();
$processedTables[] = $relatedTableName;

if ($verbose) {
echo 'Deleted ' . count($dependentRecords) . " dependent records from {$relatedTableName} where {$relatedColumn} matched deleted primary keys.\n";
}
}
}
}
}
}
}
}

/**
* Determines the primary key of a table (prioritizing `uuid`, falling back to `id`).
*
* @return string|null
*/
protected static function getPrimaryKey(array $columns)
{
return in_array('uuid', $columns) ? 'uuid' : (in_array('id', $columns) ? 'id' : null);
}

/**
* Checks if a column is likely a foreign key referencing another table.
*
* @return bool
*/
protected static function isForeignKey(string $column, string $relatedTable)
{
return str_ends_with($column, '_id') || str_ends_with($column, '_uuid') || strpos($column, $relatedTable) !== false;
}
}
110 changes: 93 additions & 17 deletions src/Support/SqlDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static function createCompanyDump($company, ?string $fileName = null)
}

/**
* Generates the SQL dump string for the company data.
* Generates the SQL dump string for the company data, including foreign key relationships.
*
* @param \Fleetbase\Models\Company $company
*
Expand All @@ -58,34 +58,110 @@ public static function getCompanyDumpSql($company)
$tables = Schema::getAllTables();
$dbPrefix = DB::getTablePrefix();

// Store related records by their primary keys
$relatedRecords = [];

foreach ($tables as $table) {
$tableName = $table->{"{$dbPrefix}Tables_in_" . $config['database']};
$columns = Schema::getColumnListing($tableName);

if (!in_array('company_uuid', $columns)) {
if (in_array($tableName, ['activity', 'api_events', 'api_request_logs', 'webhook_request_logs', 'carts'])) {
continue;
}

$records = DB::table($tableName)->where('company_uuid', $companyUuid)->get();

if ($records->isEmpty()) {
continue;
$columns = Schema::getColumnListing($tableName);
if (in_array('company_uuid', $columns)) {
// Get records related to the company
$records = DB::table($tableName)->where('company_uuid', $companyUuid)->get();

if ($records->isEmpty()) {
continue;
}

$dump .= "-- Dumping data for table `{$tableName}`\n";
foreach ($records as $record) {
$values = self::formatRecordValues((array) $record);
$columnsList = implode(', ', array_keys((array) $record));
$valuesList = implode(', ', $values);
$dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n";

// ✅ Store primary keys to track dependencies
$primaryKey = self::getPrimaryKey($columns);
if ($primaryKey && isset($record->{$primaryKey})) {
$relatedRecords[$tableName][] = $record->{$primaryKey};
}
}
$dump .= "\n";
}
}

$dump .= "-- Dumping data for table `{$tableName}`\n";
foreach ($records as $record) {
$values = array_map(function ($value) {
return is_null($value) ? 'NULL' : "'" . addslashes($value) . "'";
}, (array) $record);
// Handle dependent records based on foreign keys
foreach ($tables as $table) {
$tableName = $table->{"{$dbPrefix}Tables_in_" . $config['database']};
$columns = Schema::getColumnListing($tableName);

$columnsList = implode(', ', array_keys((array) $record));
$valuesList = implode(', ', $values);
$dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n";
foreach ($columns as $column) {
foreach ($relatedRecords as $relatedTable => $primaryKeys) {
if (self::isForeignKey($column, $relatedTable)) {
// Fetch dependent records where the column value matches primary keys
$records = DB::table($tableName)
->whereIn($column, $primaryKeys)
->get();

if ($records->isEmpty()) {
continue;
}

$dump .= "-- Dumping dependent data for table `{$tableName}` (linked to `{$relatedTable}` via `{$column}`)\n";
foreach ($records as $record) {
$values = self::formatRecordValues((array) $record);
$columnsList = implode(', ', array_keys((array) $record));
$valuesList = implode(', ', $values);
$dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n";

// ✅ Track additional primary keys for deeper dependencies
$primaryKey = self::getPrimaryKey($columns);
if ($primaryKey && isset($record->{$primaryKey})) {
$relatedRecords[$tableName][] = $record->{$primaryKey};
}
}
$dump .= "\n";
}
}
}
$dump .= "\n";
}
}

return $dump;
}

/**
* Determines the primary key of a table (prioritizing `uuid`, falling back to `id`).
*
* @return string|null
*/
protected static function getPrimaryKey(array $columns)
{
return in_array('uuid', $columns) ? 'uuid' : (in_array('id', $columns) ? 'id' : null);
}

/**
* Checks if a column is likely a foreign key referencing another table.
*
* @return bool
*/
protected static function isForeignKey(string $column, string $relatedTable)
{
return str_ends_with($column, '_id') || str_ends_with($column, '_uuid') || strpos($column, $relatedTable) !== false;
}

/**
* Formats record values for SQL insertion.
*
* @return array
*/
protected static function formatRecordValues(array $record)
{
return array_map(function ($value) {
return is_null($value) ? 'NULL' : "'" . addslashes($value) . "'";
}, $record);
}
}

0 comments on commit be8d40a

Please sign in to comment.