private/export/Write-ZtDatabaseLog.ps1

function Write-ZtDatabaseLog {
    <#
    .SYNOPSIS
        Appends a progress entry to the database import progress log.
 
    .DESCRIPTION
        Appends a single line to _database_progress.log in the logs folder, recording when
        each table import starts, completes, or fails. This append-only log provides an
        at-a-glance timeline of all database table imports and makes it easy to identify
        which table import caused an issue.
 
        Uses a per-file named mutex plus [System.IO.File]::AppendAllText for
        consistency with the export and test progress log patterns.
 
    .PARAMETER TableName
        The name of the database table being imported.
 
    .PARAMETER LogsPath
        Path to the logs folder. If empty or null, the function is a no-op.
 
    .PARAMETER Action
        The progress action: Started, Completed, or Failed.
 
    .PARAMETER Duration
        The table import duration (for Completed/Failed actions).
 
    .PARAMETER ErrorMessage
        The error message (for Failed actions).
 
    .EXAMPLE
        PS C:\> Write-ZtDatabaseLog -TableName 'User' -LogsPath $logsPath -Action Started
 
        Appends a STARTED line for the User table import to the database progress log.
 
    .EXAMPLE
        PS C:\> Write-ZtDatabaseLog -TableName 'User' -LogsPath $logsPath -Action Completed -Duration $elapsed
 
        Appends a COMPLETED line for the User table import to the database progress log.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $TableName,

        [string]
        $LogsPath,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Started', 'Completed', 'Failed')]
        [string]
        $Action,

        [timespan]
        $Duration,

        $ErrorMessage
    )
    process {
        if (-not $LogsPath) { return }

        try {
            [void][System.IO.Directory]::CreateDirectory($LogsPath)

            $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
            $actionPadded = $Action.ToUpper().PadRight(10)

            $line = "$timestamp $actionPadded $TableName"
            if ($null -ne $Duration) {
                $line += " $($Duration.ToString('hh\:mm\:ss\.fff'))"
            }
            if ($Action -eq 'Failed' -and $ErrorMessage) {
                $errorText = "$ErrorMessage"
                $errorText = $errorText -replace '[\r\n\t]+', ' '
                if ($errorText.Length -gt 1000) {
                    $errorText = $errorText.Substring(0, 1000) + '...'
                }
                $line += " $errorText"
            }
            $line += [System.Environment]::NewLine

            $progressFilePath = Join-Path $LogsPath '_database_progress.log'
            $fullPath = [System.IO.Path]::GetFullPath($progressFilePath)
            $normalizedPath = if ($IsWindows) { $fullPath.ToLowerInvariant() } else { $fullPath }

            # Cache the mutex name per resolved path to avoid repeated SHA256 hashing
            if (-not $script:ZtDatabaseProgressMutexCache) {
                $script:ZtDatabaseProgressMutexCache = @{}
            }
            if ($script:ZtDatabaseProgressMutexCache.ContainsKey($normalizedPath)) {
                $mutexName = $script:ZtDatabaseProgressMutexCache[$normalizedPath]
            }
            else {
                $pathBytes = [System.Text.Encoding]::UTF8.GetBytes($normalizedPath)
                $pathHashBytes = [System.Security.Cryptography.SHA256]::HashData($pathBytes)
                $pathHash = [System.BitConverter]::ToString($pathHashBytes).Replace('-', '')
                $mutexName = "Local\ZtDatabaseProgress_$pathHash"
                $script:ZtDatabaseProgressMutexCache[$normalizedPath] = $mutexName
            }

            $mutex = $null
            $lockAcquired = $false
            try {
                $mutex = [System.Threading.Mutex]::new($false, $mutexName)
                $lockAcquired = $mutex.WaitOne([TimeSpan]::FromSeconds(5))
                if (-not $lockAcquired) {
                    throw "Timed out waiting for database progress log mutex '$mutexName'."
                }

                [System.IO.File]::AppendAllText($fullPath, $line)
            }
            finally {
                if ($lockAcquired -and $null -ne $mutex) {
                    $null = $mutex.ReleaseMutex()
                }
                if ($null -ne $mutex) {
                    $mutex.Dispose()
                }
            }
        }
        catch {
            Write-PSFMessage -Level Warning -Message "Failed to write database progress log for '{0}': {1}" -StringValues $TableName, $_.Exception.Message -Tag log
        }
    }
}