2026-04-22 18:41:37 +02:00
param (
2026-04-22 22:45:44 +02:00
[ 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 ) ,
2026-04-22 18:41:37 +02:00
[ 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 + + ) {
2026-04-22 22:45:44 +02:00
$logs = & docker logs $Name 2 > & 1
if ( $LASTEXITCODE -eq 0 -and ( $logs -join " `n " ) -match 'ready for connections.*port: 3306' ) {
return
2026-04-22 18:41:37 +02:00
}
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
)
2026-04-22 22:45:44 +02:00
$dockerArgs = @ ( 'exec' , '-e' , " MYSQL_PWD= $RootPassword " , $ContainerName , 'mysql' , '-N' , '-B' , '-uroot' )
2026-04-22 18:41:37 +02:00
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 ( '`' , '``' ) + '`'
}
2026-04-22 22:45:44 +02:00
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 ','
2026-04-22 18:41:37 +02:00
}
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
2026-04-22 22:45:44 +02:00
$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 " )
}
}
2026-04-22 18:41:37 +02:00
$importErrorLogFileName = [ System.IO.Path ] :: GetFileNameWithoutExtension ( $sourceFileName ) + '.import-errors.log'
$importErrorLogPath = Join-Path $dbDirectory $importErrorLogFileName
$sourceSizeBytes = ( Get-Item $SourceDump ) . Length
2026-04-22 22:45:44 +02:00
$keepRaceListSql = ConvertTo-SqlIntList -Values $KeepRaceIds
$keepUserListSql = ConvertTo-SqlIntList -Values $KeepUserIds
2026-04-22 18:41:37 +02:00
Write-Host " Using source dump: $SourceDump "
Write-Host " Source size: $sourceSizeBytes bytes "
Write-Host " Import error log: $importErrorLogPath "
2026-04-22 22:45:44 +02:00
if ( $overlayResolvedPath ) {
Write-Host " Using overlay dump: $overlayResolvedPath "
}
Write-Host " KeepRaceIdsRequested= $keepRaceListSql "
Write-Host " KeepUserIdsRequested= $keepUserListSql "
2026-04-22 18:41:37 +02:00
try {
& docker rm -f $ContainerName 1 > $null 2 > $null
& docker volume rm $VolumeName 1 > $null 2 > $null
2026-04-22 22:45:44 +02:00
$dockerRunArgs = @ (
2026-04-22 18:41:37 +02:00
'run' , '-d' , '--name' , $ContainerName ,
'-e' , " MYSQL_ROOT_PASSWORD= $RootPassword " ,
2026-04-22 22:45:44 +02:00
'-e' , 'MYSQL_ROOT_HOST=%' ,
2026-04-22 18:41:37 +02:00
'-v' , " ${dbDirectory} :/workspace/db " ,
2026-04-22 22:45:44 +02:00
'-v' , " ${VolumeName} :/var/lib/mysql "
)
if ( $overlayMount . Count -gt 0 ) {
$dockerRunArgs + = $overlayMount
}
$dockerRunArgs + = @ (
2026-04-22 18:41:37 +02:00
'mysql:8.4' ,
'--max_allowed_packet=1G' ,
'--net_read_timeout=600' ,
'--net_write_timeout=600'
2026-04-22 22:45:44 +02:00
)
Invoke-DockerCapture -DockerArgs $dockerRunArgs | Out-Null
Write-Host 'Temporary MySQL container started.'
2026-04-22 18:41:37 +02:00
Wait-ForMysqlReady -Name $ContainerName -Password $RootPassword
2026-04-22 22:45:44 +02:00
Write-Host 'Temporary MySQL is ready.'
2026-04-22 18:41:37 +02:00
Invoke-MysqlQuery -SkipDatabase -Query " DROP DATABASE IF EXISTS $DatabaseName ; CREATE DATABASE $DatabaseName CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; " | Out-Null
2026-04-22 22:45:44 +02:00
Write-Host 'Temporary database created.'
2026-04-22 18:41:37 +02:00
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 "
)
2026-04-22 22:45:44 +02:00
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;' )
2026-04-22 18:41:37 +02:00
$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
2026-04-22 22:45:44 +02:00
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 ) ;
2026-04-22 18:41:37 +02:00
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
2026-04-22 22:45:44 +02:00
$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 ',' ) "
}
2026-04-22 18:41:37 +02:00
$cleanupStats = [ System.Collections.Generic.List[string] ] :: new ( )
Invoke-MysqlQuery -Query 'DELETE FROM log_foto;' | Out-Null
$cleanupStats . Add ( 'log_foto:deleted-all' )
2026-04-22 22:45:44 +02:00
$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 " )
}
}
2026-04-22 18:41:37 +02:00
$fotoDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @"
SELECT table_name
FROM information_schema . columns
WHERE table_schema = '$DatabaseName'
AND column_name = 'id_foto'
2026-04-22 22:45:44 +02:00
AND table_name NOT IN ( 'foto' , 'log_foto' , 'keep_foto_ids' , 'keep_gara_ids' , 'keep_punto_foto_ids' , 'keep_user_ids' )
2026-04-22 18:41:37 +02:00
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 " )
}
2026-04-22 22:45:44 +02:00
$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 = @ ( )
2026-04-22 18:41:37 +02:00
if ( $usersExists ) {
Invoke-MysqlQuery -Query @"
DROP TABLE IF EXISTS keep_user_ids ;
CREATE TABLE keep_user_ids AS
SELECT id_users
FROM users
2026-04-22 22:45:44 +02:00
WHERE id_users IN ( $keepUserListSql )
ORDER BY FIELD ( id_users , $keepUserListSql ) ;
2026-04-22 18:41:37 +02:00
" @ | Out-Null
2026-04-22 22:45:44 +02:00
$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 ',' ) "
}
2026-04-22 18:41:37 +02:00
$usersDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @"
SELECT table_name
FROM information_schema . columns
WHERE table_schema = '$DatabaseName'
AND column_name = 'id_users'
2026-04-22 22:45:44 +02:00
AND table_name NOT IN ( 'users' , 'keep_user_ids' , 'keep_gara_ids' , 'keep_punto_foto_ids' , 'keep_foto_ids' )
2026-04-22 18:41:37 +02:00
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 " )
}
}
2026-04-22 22:45:44 +02:00
$afterGara = [ int64 ] ( Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara;' )
$afterPuntoFoto = [ int64 ] ( Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto;' )
2026-04-22 18:41:37 +02:00
$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
}
2026-04-22 22:45:44 +02:00
Write-Host " Exporting curated dump... "
2026-04-22 18:41:37 +02:00
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 ) ) {
2026-04-22 22:45:44 +02:00
throw " Curated dump was not created: $OutputDump "
2026-04-22 18:41:37 +02:00
}
$outputSizeBytes = ( Get-Item $OutputDump ) . Length
Write-Host " OriginalSizeBytes= $sourceSizeBytes "
2026-04-22 22:45:44 +02:00
Write-Host " CuratedSizeBytes= $outputSizeBytes "
Write-Host " BeforeGara= $beforeGara "
Write-Host " AfterGara= $afterGara "
Write-Host " BeforePuntoFoto= $beforePuntoFoto "
Write-Host " AfterPuntoFoto= $afterPuntoFoto "
2026-04-22 18:41:37 +02:00
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 ',' ) )
2026-04-22 22:45:44 +02:00
if ( $usersExists ) {
Write-Host ( " KeptUserIds= " + ( ( $keptUserIds | ForEach-Object { $_ . Trim ( ) } ) -join ',' ) )
}
2026-04-22 18:41:37 +02:00
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
}