param( [string]$SourceDump = (Join-Path $PSScriptRoot '..\db\pg-model-seed-trimmed-20260421.sql'), [string]$OverlayDump = (Join-Path $PSScriptRoot '..\db\pg-local-model-fixtures-overlay-20260422.sql'), [string]$OutputDump = (Join-Path $PSScriptRoot '..\db\pg-local-purpose-seed-20260422.sql'), [int[]]$KeepRaceIds = @(1018547, 1018557), [int[]]$KeepUserIds = @(2), [string]$ContainerName = 'regalami-dump-trim-mysql-temp', [string]$VolumeName = 'regalami-dump-trim-mysql-temp-data', [string]$DatabaseName = 'pgtrim', [string]$RootPassword = 'root' ) $ErrorActionPreference = 'Stop' function Invoke-DockerCapture { param( [Parameter(Mandatory = $true)] [string[]]$DockerArgs ) $output = & docker @DockerArgs 2>&1 if ($LASTEXITCODE -ne 0) { throw (("docker " + ($DockerArgs -join ' ')) + " failed:`n" + ($output -join "`n")) } return $output } function Invoke-DockerQuiet { param( [Parameter(Mandatory = $true)] [string[]]$DockerArgs ) & docker @DockerArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "docker $($DockerArgs -join ' ') failed" } } function Wait-ForMysqlReady { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Password ) for ($attempt = 0; $attempt -lt 180; $attempt++) { $logs = & docker logs $Name 2>&1 if ($LASTEXITCODE -eq 0 -and ($logs -join "`n") -match 'ready for connections.*port: 3306') { return } Start-Sleep -Seconds 2 } throw "MySQL in container $Name did not become ready in time." } function Invoke-MysqlQuery { param( [Parameter(Mandatory = $true)] [string]$Query, [switch]$SkipDatabase ) $dockerArgs = @('exec', '-e', "MYSQL_PWD=$RootPassword", $ContainerName, 'mysql', '-N', '-B', '-uroot') if (-not $SkipDatabase) { $dockerArgs += @('-D', $DatabaseName) } $dockerArgs += @('-e', $Query) return Invoke-DockerCapture -DockerArgs $dockerArgs } function Get-SqlScalar { param( [Parameter(Mandatory = $true)] [string]$Query, [switch]$SkipDatabase ) $result = Invoke-MysqlQuery -Query $Query -SkipDatabase:$SkipDatabase if (-not $result) { return '' } return ($result | Select-Object -First 1).Trim() } function Quote-Identifier { param([string]$Name) return '`' + $Name.Replace('`', '``') + '`' } function ConvertTo-SqlIntList { param( [Parameter(Mandatory = $true)] [int[]]$Values ) $dedupedValues = $Values | Sort-Object -Unique if (-not $dedupedValues -or $dedupedValues.Count -eq 0) { throw 'At least one numeric keep id is required.' } return ($dedupedValues | ForEach-Object { [string]$_ }) -join ',' } if (-not (Test-Path $SourceDump)) { throw "Source dump not found: $SourceDump" } $dbDirectory = Split-Path -Parent (Resolve-Path $SourceDump).Path $sourceFileName = Split-Path -Leaf $SourceDump $outputFileName = Split-Path -Leaf $OutputDump $overlayResolvedPath = $null $overlayFileName = '' $overlayDirectory = $dbDirectory $overlayMount = @() if ($OverlayDump -and (Test-Path $OverlayDump)) { $overlayResolvedPath = (Resolve-Path $OverlayDump).Path $overlayFileName = Split-Path -Leaf $overlayResolvedPath $overlayDirectory = Split-Path -Parent $overlayResolvedPath if ($overlayDirectory -ne $dbDirectory) { $overlayMount = @('-v', "${overlayDirectory}:/workspace/overlay") } } $importErrorLogFileName = [System.IO.Path]::GetFileNameWithoutExtension($sourceFileName) + '.import-errors.log' $importErrorLogPath = Join-Path $dbDirectory $importErrorLogFileName $sourceSizeBytes = (Get-Item $SourceDump).Length $keepRaceListSql = ConvertTo-SqlIntList -Values $KeepRaceIds $keepUserListSql = ConvertTo-SqlIntList -Values $KeepUserIds Write-Host "Using source dump: $SourceDump" Write-Host "Source size: $sourceSizeBytes bytes" Write-Host "Import error log: $importErrorLogPath" if ($overlayResolvedPath) { Write-Host "Using overlay dump: $overlayResolvedPath" } Write-Host "KeepRaceIdsRequested=$keepRaceListSql" Write-Host "KeepUserIdsRequested=$keepUserListSql" try { & docker rm -f $ContainerName 1>$null 2>$null & docker volume rm $VolumeName 1>$null 2>$null $dockerRunArgs = @( 'run', '-d', '--name', $ContainerName, '-e', "MYSQL_ROOT_PASSWORD=$RootPassword", '-e', 'MYSQL_ROOT_HOST=%', '-v', "${dbDirectory}:/workspace/db", '-v', "${VolumeName}:/var/lib/mysql" ) if ($overlayMount.Count -gt 0) { $dockerRunArgs += $overlayMount } $dockerRunArgs += @( 'mysql:8.4', '--max_allowed_packet=1G', '--net_read_timeout=600', '--net_write_timeout=600' ) Invoke-DockerCapture -DockerArgs $dockerRunArgs | Out-Null Write-Host 'Temporary MySQL container started.' Wait-ForMysqlReady -Name $ContainerName -Password $RootPassword Write-Host 'Temporary MySQL is ready.' Invoke-MysqlQuery -SkipDatabase -Query "DROP DATABASE IF EXISTS $DatabaseName; CREATE DATABASE $DatabaseName CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;" | Out-Null Write-Host 'Temporary database created.' Write-Host "Importing source dump into temporary MySQL database..." if (Test-Path $importErrorLogPath) { Remove-Item $importErrorLogPath -Force } Invoke-DockerQuiet -DockerArgs @( 'exec', $ContainerName, 'sh', '-lc', "mysql --force -uroot -p$RootPassword $DatabaseName < /workspace/db/$sourceFileName 2> /workspace/db/$importErrorLogFileName" ) if ($overlayResolvedPath) { $overlayContainerPath = if ($overlayDirectory -eq $dbDirectory) { "/workspace/db/$overlayFileName" } else { "/workspace/overlay/$overlayFileName" } Write-Host "Importing overlay dump into temporary MySQL database..." Invoke-DockerQuiet -DockerArgs @( 'exec', $ContainerName, 'sh', '-lc', "mysql --force -uroot -p$RootPassword $DatabaseName < $overlayContainerPath" ) } $beforeGara = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara;') $beforePuntoFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto;') $beforeFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM foto;') $beforeLogFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM log_foto;') $usersExists = (Get-SqlScalar -SkipDatabase -Query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '$DatabaseName' AND table_name = 'users';") -eq '1' $beforeUsers = 0 if ($usersExists) { $beforeUsers = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM users;') } Invoke-MysqlQuery -Query @" DROP TABLE IF EXISTS keep_gara_ids; CREATE TABLE keep_gara_ids AS SELECT id_gara FROM gara WHERE id_gara IN ($keepRaceListSql) ORDER BY FIELD(id_gara, $keepRaceListSql); DROP TABLE IF EXISTS keep_punto_foto_ids; CREATE TABLE keep_punto_foto_ids AS SELECT id_puntoFoto FROM punto_foto WHERE id_gara IN (SELECT id_gara FROM keep_gara_ids); DROP TABLE IF EXISTS keep_foto_ids; CREATE TABLE keep_foto_ids AS SELECT id_foto FROM foto WHERE id_gara IN (SELECT id_gara FROM keep_gara_ids); "@ | Out-Null $keptGaraIds = Invoke-MysqlQuery -Query 'SELECT id_gara FROM keep_gara_ids ORDER BY id_gara;' $missingRaceIds = @() foreach ($requestedRaceId in ($KeepRaceIds | Sort-Object -Unique)) { if (-not (($keptGaraIds | ForEach-Object { $_.Trim() }) -contains [string]$requestedRaceId)) { $missingRaceIds += $requestedRaceId } } if ($missingRaceIds.Count -gt 0) { throw "One or more requested race ids are missing from the imported data: $($missingRaceIds -join ',')" } $cleanupStats = [System.Collections.Generic.List[string]]::new() Invoke-MysqlQuery -Query 'DELETE FROM log_foto;' | Out-Null $cleanupStats.Add('log_foto:deleted-all') $garaDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" SELECT table_name FROM information_schema.columns WHERE table_schema = '$DatabaseName' AND column_name = 'id_gara' AND table_name NOT IN ('gara', 'foto', 'punto_foto', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_foto_ids', 'keep_user_ids') ORDER BY table_name; "@ foreach ($tableName in $garaDependentTables) { if (-not $tableName) { continue } $trimmedTableName = $tableName.Trim() $quotedTable = Quote-Identifier $trimmedTableName $deletedRows = [int64](Get-SqlScalar -Query "SELECT COUNT(*) FROM $quotedTable WHERE id_gara IS NOT NULL AND id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);") if ($deletedRows -gt 0) { Invoke-MysqlQuery -Query "DELETE FROM $quotedTable WHERE id_gara IS NOT NULL AND id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);" | Out-Null $cleanupStats.Add("$($trimmedTableName):$deletedRows") } } $fotoDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" SELECT table_name FROM information_schema.columns WHERE table_schema = '$DatabaseName' AND column_name = 'id_foto' AND table_name NOT IN ('foto', 'log_foto', 'keep_foto_ids', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_user_ids') ORDER BY table_name; "@ foreach ($tableName in $fotoDependentTables) { if (-not $tableName) { continue } $trimmedTableName = $tableName.Trim() $quotedTable = Quote-Identifier $trimmedTableName $deletedRows = [int64](Get-SqlScalar -Query "SELECT COUNT(*) FROM $quotedTable WHERE id_foto IS NOT NULL AND id_foto NOT IN (SELECT id_foto FROM keep_foto_ids);") if ($deletedRows -gt 0) { Invoke-MysqlQuery -Query "DELETE FROM $quotedTable WHERE id_foto IS NOT NULL AND id_foto NOT IN (SELECT id_foto FROM keep_foto_ids);" | Out-Null $cleanupStats.Add("$($trimmedTableName):$deletedRows") } } $deletedFotoRows = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM foto WHERE id_foto NOT IN (SELECT id_foto FROM keep_foto_ids);') if ($deletedFotoRows -gt 0) { Invoke-MysqlQuery -Query 'DELETE FROM foto WHERE id_foto NOT IN (SELECT id_foto FROM keep_foto_ids);' | Out-Null $cleanupStats.Add("foto:$deletedFotoRows") } $deletedPuntoFotoRows = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);') if ($deletedPuntoFotoRows -gt 0) { Invoke-MysqlQuery -Query 'DELETE FROM punto_foto WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);' | Out-Null $cleanupStats.Add("punto_foto:$deletedPuntoFotoRows") } $deletedGaraRows = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);') if ($deletedGaraRows -gt 0) { Invoke-MysqlQuery -Query 'DELETE FROM gara WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);' | Out-Null $cleanupStats.Add("gara:$deletedGaraRows") } $keptUserIds = @() if ($usersExists) { Invoke-MysqlQuery -Query @" DROP TABLE IF EXISTS keep_user_ids; CREATE TABLE keep_user_ids AS SELECT id_users FROM users WHERE id_users IN ($keepUserListSql) ORDER BY FIELD(id_users, $keepUserListSql); "@ | Out-Null $keptUserIds = Invoke-MysqlQuery -Query 'SELECT id_users FROM keep_user_ids ORDER BY id_users;' $missingUserIds = @() foreach ($requestedUserId in ($KeepUserIds | Sort-Object -Unique)) { if (-not (($keptUserIds | ForEach-Object { $_.Trim() }) -contains [string]$requestedUserId)) { $missingUserIds += $requestedUserId } } if ($missingUserIds.Count -gt 0) { throw "One or more requested user ids are missing from the imported data: $($missingUserIds -join ',')" } $usersDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" SELECT table_name FROM information_schema.columns WHERE table_schema = '$DatabaseName' AND column_name = 'id_users' AND table_name NOT IN ('users', 'keep_user_ids', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_foto_ids') ORDER BY table_name; "@ foreach ($tableName in $usersDependentTables) { if (-not $tableName) { continue } $trimmedTableName = $tableName.Trim() $quotedTable = Quote-Identifier $trimmedTableName $deletedRows = [int64](Get-SqlScalar -Query "SELECT COUNT(*) FROM $quotedTable WHERE id_users IS NOT NULL AND id_users NOT IN (SELECT id_users FROM keep_user_ids);") if ($deletedRows -gt 0) { Invoke-MysqlQuery -Query "DELETE FROM $quotedTable WHERE id_users IS NOT NULL AND id_users NOT IN (SELECT id_users FROM keep_user_ids);" | Out-Null $cleanupStats.Add("$($trimmedTableName):$deletedRows") } } $deletedUsers = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM users WHERE id_users NOT IN (SELECT id_users FROM keep_user_ids);') if ($deletedUsers -gt 0) { Invoke-MysqlQuery -Query 'DELETE FROM users WHERE id_users NOT IN (SELECT id_users FROM keep_user_ids);' | Out-Null $cleanupStats.Add("users:$deletedUsers") } } $afterGara = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara;') $afterPuntoFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto;') $afterFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM foto;') $afterLogFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM log_foto;') $afterUsers = 0 if ($usersExists) { $afterUsers = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM users;') } if (Test-Path $OutputDump) { Remove-Item $OutputDump -Force } Write-Host "Exporting curated dump..." Invoke-DockerQuiet -DockerArgs @( 'exec', $ContainerName, 'sh', '-lc', "mysqldump -uroot -p$RootPassword --default-character-set=utf8mb4 --single-transaction --routines --triggers --set-gtid-purged=OFF $DatabaseName > /workspace/db/$outputFileName" ) if (-not (Test-Path $OutputDump)) { throw "Curated dump was not created: $OutputDump" } $outputSizeBytes = (Get-Item $OutputDump).Length Write-Host "OriginalSizeBytes=$sourceSizeBytes" Write-Host "CuratedSizeBytes=$outputSizeBytes" Write-Host "BeforeGara=$beforeGara" Write-Host "AfterGara=$afterGara" Write-Host "BeforePuntoFoto=$beforePuntoFoto" Write-Host "AfterPuntoFoto=$afterPuntoFoto" Write-Host "BeforeFoto=$beforeFoto" Write-Host "AfterFoto=$afterFoto" Write-Host "BeforeLogFoto=$beforeLogFoto" Write-Host "AfterLogFoto=$afterLogFoto" Write-Host "BeforeUsers=$beforeUsers" Write-Host "AfterUsers=$afterUsers" Write-Host ("KeptGaraIds=" + (($keptGaraIds | ForEach-Object { $_.Trim() }) -join ',')) if ($usersExists) { Write-Host ("KeptUserIds=" + (($keptUserIds | ForEach-Object { $_.Trim() }) -join ',')) } Write-Host ("DependencyCleanup=" + (($cleanupStats | Sort-Object) -join ';')) if (Test-Path $importErrorLogPath) { $importErrorCount = (Get-Item $importErrorLogPath).Length Write-Host "ImportErrorLogBytes=$importErrorCount" } } finally { & docker rm -f $ContainerName 1>$null 2>$null & docker volume rm $VolumeName 1>$null 2>$null }