param( [string]$OldNamespace = "ircs-system", [string]$NewNamespace = "ircs-prod", [string]$Database = "ircs", [switch]$Execute, [switch]$TruncateTarget ) $ErrorActionPreference = "Stop" $tables = @( 'actors', 'collection_tasks', 'cover_images', 'data_sources', 'directors', 'episodes', 'failed_messages', 'magnet_links', 'magnet_provider_runs', 'magnet_providers', 'magnet_search_jobs', 'member_favorites', 'member_watch_histories', 'members', 'playlists', 'raw_areas', 'raw_category', 'raw_genres', 'raw_languages', 'raw_video_unified_video', 'raw_videos', 'request_audit_logs', 'search_sync_tasks', 'source_domains', 'standard_areas', 'standard_category', 'standard_genre', 'standard_languages', 'sys_credentials', 'system_configs', 'unified_video_actors', 'unified_video_directors', 'unified_video_genres', 'unified_video_standard_areas', 'unified_video_standard_languages', 'unified_videos', 'user_messages', 'video_actors', 'video_directors', 'video_raw_areas', 'video_raw_genres', 'video_raw_languages', 'video_resolver_sources' ) function Invoke-Postgres($Namespace, $Sql) { $escapedSql = $Sql.Replace("'", "'\''") kubectl -n $Namespace exec statefulset/postgres -- sh -lc "PGPASSWORD=\`$POSTGRES_PASSWORD psql -U postgres -d $Database -Atc '$escapedSql'" } function Get-TableColumns($Namespace, $TableName) { $sql = "select column_name || '|' || udt_name || '|' || is_nullable || '|' || coalesce(column_default, '') from information_schema.columns where table_schema='public' and table_name='$TableName' order by ordinal_position" $lines = @(Invoke-Postgres $Namespace $sql) $columns = @() foreach ($line in $lines) { if (-not $line) { continue } $parts = $line -split '\|', 4 $columns += [pscustomobject]@{ Name = $parts[0] UdtName = $parts[1] Nullable = $parts[2] Default = if ($parts.Count -ge 4) { $parts[3] } else { '' } } } return $columns } Write-Host "Compatible business tables planned for migration: $($tables.Count)" Write-Host "Excluded tables: databasechangelog, databasechangeloglock, V3 runtime audit/outbox/maintenance derived tables." $oldTables = Invoke-Postgres $OldNamespace "select tablename from pg_tables where schemaname='public' order by tablename" $newTables = Invoke-Postgres $NewNamespace "select tablename from pg_tables where schemaname='public' order by tablename" $missingOld = $tables | Where-Object { $_ -notin $oldTables } $missingNew = $tables | Where-Object { $_ -notin $newTables } if ($missingOld) { throw "Old namespace is missing expected tables: $($missingOld -join ', ')" } if ($missingNew) { throw "New namespace is missing expected tables. Run ircs-prod core migrator first: $($missingNew -join ', ')" } $schemaIssues = @() foreach ($table in $tables) { $oldColumns = @(Get-TableColumns $OldNamespace $table) $newColumns = @(Get-TableColumns $NewNamespace $table) $newByName = @{} foreach ($column in $newColumns) { $newByName[$column.Name] = $column } $oldByName = @{} foreach ($column in $oldColumns) { $oldByName[$column.Name] = $column if (-not $newByName.ContainsKey($column.Name)) { $schemaIssues += "${table}: old column '$($column.Name)' is missing in target" continue } $target = $newByName[$column.Name] if ($column.UdtName -ne $target.UdtName) { $schemaIssues += "$table.$($column.Name): type mismatch old=$($column.UdtName) new=$($target.UdtName)" } } foreach ($column in $newColumns) { if (-not $oldByName.ContainsKey($column.Name) -and $column.Nullable -eq 'NO' -and -not $column.Default) { $schemaIssues += "${table}: target required column '$($column.Name)' has no old value and no default" } } } if ($schemaIssues.Count -gt 0) { $schemaIssues | ForEach-Object { Write-Host "SCHEMA ISSUE: $_" } throw "Schema preflight failed with $($schemaIssues.Count) issue(s)." } Write-Host "Schema preflight passed for $($tables.Count) compatible business tables." if (-not $Execute) { Write-Host "Dry run only. Add -Execute to stream data from $OldNamespace to $NewNamespace." exit 0 } if ($TruncateTarget) { $quoted = ($tables | ForEach-Object { 'public."' + $_ + '"' }) -join ', ' Invoke-Postgres $NewNamespace "truncate table $quoted restart identity cascade" | Out-Null } $tableArgs = ($tables | ForEach-Object { "--table=public.$_" }) -join ' ' $dumpCmd = "PGPASSWORD=\`$POSTGRES_PASSWORD pg_dump -U postgres -d $Database --data-only --column-inserts --disable-triggers $tableArgs" $loadCmd = "PGPASSWORD=\`$POSTGRES_PASSWORD psql -U postgres -d $Database -v ON_ERROR_STOP=1" kubectl -n $OldNamespace exec statefulset/postgres -- sh -lc $dumpCmd | kubectl -n $NewNamespace exec -i statefulset/postgres -- sh -lc $loadCmd Write-Host "Compatible data migration stream finished."