param( [string]$SourcePath = (Join-Path $PSScriptRoot '..\sql_statements.txt') ) $ErrorActionPreference = 'Stop' $source = $SourcePath $target = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements.sql' $targetWithSkipped = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_with_skipped.sql' $skipReportCsv = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_skip_report.csv' function Convert-ParamLiteral { param([string]$Value) if ($null -eq $Value) { return 'NULL' } $trimmed = $Value.Trim() if ($trimmed -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { return "'$trimmed'::uuid" } if ($trimmed -match '^(?i:true|false)$') { return $trimmed.ToLowerInvariant() } if ($trimmed -match '^\d+$') { return $trimmed } $escaped = $trimmed.Replace("'", "''") return "'$escaped'" } function Apply-ParamsToSql { param( [string]$Sql, [string]$ParamsText ) $result = $Sql $normalizedParams = if ($null -eq $ParamsText) { '' } else { $ParamsText.Trim() } # Newer EF logs wrap parameter payload in quotes inside [Parameters=[...]]. if ($normalizedParams.StartsWith('"') -and $normalizedParams.EndsWith('"')) { $normalizedParams = $normalizedParams.Trim('"') } if ([string]::IsNullOrWhiteSpace($normalizedParams) -or $normalizedParams -eq '[]') { return $result } # Truncated arrays in logs contain ellipsis; these are skipped later. if ($normalizedParams -like '*...*') { return $result } $arrayPattern = [regex]"@(?[A-Za-z0-9_]+)=\{(?.*?)\}\s*\(DbType\s*=\s*Object\)" foreach ($match in $arrayPattern.Matches($normalizedParams)) { $name = $match.Groups['name'].Value $valsRaw = $match.Groups['vals'].Value $valueMatches = [regex]::Matches($valsRaw, "'([^']*)'") $renderedVals = @() foreach ($valueMatch in $valueMatches) { $renderedVals += (Convert-ParamLiteral -Value $valueMatch.Groups[1].Value) } $arrayExpr = if ($renderedVals.Count -gt 0) { 'ARRAY[' + ($renderedVals -join ', ') + ']' } else { 'ARRAY[]::text[]' } $tokenPattern = '(?[A-Za-z0-9_]+)='(?(?:''|[^'])*)'" foreach ($match in $scalarPattern.Matches($normalizedParams)) { $name = $match.Groups['name'].Value $rawVal = $match.Groups['val'].Value -replace "''", "'" $literal = Convert-ParamLiteral -Value $rawVal $tokenPattern = '(?[^\]]+)\].*Executed DbCommand\s*\("?(?\d+)"?ms\)\s*\[Parameters=\[(?.*?)\],\s*CommandType=''Text'',\s*CommandTimeout=''(?\d+)''\](?.*)$' $logLinePattern = [regex]'^\[(?:\d{2}:\d{2}:\d{2}\.\d{3}|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}(?:\s+[^\]]+)?)\]' Get-Content -Path $source | ForEach-Object { $line = $_ $cmdMatch = $commandLinePattern.Match($line) if ($cmdMatch.Success) { if ($null -ne $current) { $entries.Add([pscustomobject]@{ ms = $current.ms timeout = $current.timeout params = $current.params sql = ($current.sqlLines -join "`n").Trim() }) } $current = [ordered]@{ ms = $cmdMatch.Groups['ms'].Value timeout = $cmdMatch.Groups['timeout'].Value params = $cmdMatch.Groups['params'].Value sqlLines = New-Object System.Collections.Generic.List[string] } $tail = Normalize-SqlLogLine -Line $cmdMatch.Groups['tail'].Value if (-not [string]::IsNullOrWhiteSpace($tail)) { $current.sqlLines.Add($tail) } return } if ($null -eq $current) { return } if ($logLinePattern.IsMatch($line)) { $entries.Add([pscustomobject]@{ ms = $current.ms timeout = $current.timeout params = $current.params sql = ($current.sqlLines -join "`n").Trim() }) $current = $null return } $normalizedLine = Normalize-SqlLogLine -Line $line if (-not [string]::IsNullOrWhiteSpace($normalizedLine)) { $current.sqlLines.Add($normalizedLine) } } if ($null -ne $current) { $entries.Add([pscustomobject]@{ ms = $current.ms timeout = $current.timeout params = $current.params sql = ($current.sqlLines -join "`n").Trim() }) } $output = New-Object System.Collections.Generic.List[string] $output.Add('-- Auto-generated from sql_statements.txt for regression testing') $output.Add("-- Source: $source") $output.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')") $output.Add('-- Unresolved parameterized statements are skipped and counted in summary.') $output.Add('\\timing on') $output.Add("SET statement_timeout = '0';") $output.Add('') $outputWithSkipped = New-Object System.Collections.Generic.List[string] $outputWithSkipped.Add('-- Auto-generated from sql_statements.txt for regression testing') $outputWithSkipped.Add("-- Source: $source") $outputWithSkipped.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')") $outputWithSkipped.Add('-- Includes unresolved statements as commented blocks for manual editing.') $outputWithSkipped.Add('\\timing on') $outputWithSkipped.Add("SET statement_timeout = '0';") $outputWithSkipped.Add('') $totalExtracted = 0 $written = 0 $skipped = 0 $writtenWithSkipped = 0 $skipReasonCounts = @{} function Add-SkipReason { param([string]$Reason) if (-not $skipReasonCounts.ContainsKey($Reason)) { $skipReasonCounts[$Reason] = 0 } $skipReasonCounts[$Reason]++ } foreach ($entry in $entries) { $totalExtracted++ $sqlText = $entry.sql if ([string]::IsNullOrWhiteSpace($sqlText)) { $skipped++ Add-SkipReason -Reason 'empty_sql_block' continue } # Drop only whole-statement wrapper quotes emitted by EF logs. $sqlText = $sqlText.Trim() $sqlText = $sqlText -replace '^"+(?=(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b)', '' $sqlText = $sqlText -replace '"\s*$', '' $sqlText = Apply-ParamsToSql -Sql $sqlText -ParamsText $entry.params if ($sqlText -notmatch ';\s*$') { $sqlText += ';' } $writtenWithSkipped++ $outputWithSkipped.Add("-- Query #$writtenWithSkipped | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)") $topLevelStarts = ([regex]::Matches($sqlText, '(?m)^(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b')).Count if ($topLevelStarts -gt 1) { $skipped++ Add-SkipReason -Reason 'multiple_top_level_sql_starts' $outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: block contains multiple top-level SQL starts (likely concatenated log fragments)') foreach ($line in ($sqlText -split "`n")) { $outputWithSkipped.Add('-- ' + $line) } $outputWithSkipped.Add('') continue } if ($sqlText -match '@[A-Za-z_][A-Za-z0-9_]*') { $skipped++ Add-SkipReason -Reason 'unresolved_parameter_placeholders' $outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: unresolved placeholders remain') if (-not [string]::IsNullOrWhiteSpace($entry.params)) { $outputWithSkipped.Add("-- Logged parameters: [$($entry.params)]") } foreach ($line in ($sqlText -split "`n")) { $outputWithSkipped.Add('-- ' + $line) } $outputWithSkipped.Add('') continue } if (-not (Test-SqlStatementCompleteness -Sql $sqlText)) { $skipped++ Add-SkipReason -Reason 'syntactically_incomplete_or_truncated' $outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: statement appears truncated or syntactically incomplete') foreach ($line in ($sqlText -split "`n")) { $outputWithSkipped.Add('-- ' + $line) } $outputWithSkipped.Add('') continue } $written++ $output.Add("-- Query #$written | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)") $output.Add($sqlText) $output.Add('') $outputWithSkipped.Add($sqlText) $outputWithSkipped.Add('') } $output.Add("-- Summary: total_extracted=$totalExtracted, written=$written, skipped_unresolved=$skipped") $outputWithSkipped.Add("-- Summary: total_extracted=$totalExtracted, written_total=$writtenWithSkipped, unresolved_commented=$skipped") $skipReasonRows = @() foreach ($key in ($skipReasonCounts.Keys | Sort-Object)) { $count = [int]$skipReasonCounts[$key] $pct = if ($skipped -gt 0) { [math]::Round(($count * 100.0) / $skipped, 2) } else { 0.0 } $skipReasonRows += [pscustomobject]@{ reason = $key count = $count percent_of_skipped = $pct } } $skipReasonRows | Export-Csv -Path $skipReportCsv -NoTypeInformation -Encoding UTF8 Set-Content -Path $target -Value $output -Encoding UTF8 Set-Content -Path $targetWithSkipped -Value $outputWithSkipped -Encoding UTF8 Write-Output "Generated: $target" Write-Output "Generated: $targetWithSkipped" Write-Output "Generated: $skipReportCsv" Write-Output "Total extracted: $totalExtracted" Write-Output "Written runnable: $written" Write-Output "Skipped unresolved: $skipped"