param( [string]$Server = $env:ONEC_SQL_SERVER, [string]$Database = $env:ONEC_SQL_DATABASE, [string]$User = $env:ONEC_SQL_USER, [string]$Password = $env:ONEC_SQL_PASSWORD, [string]$ReadResultPath, [string]$RouteIndexPath, [string]$OutputPath ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" if (-not $Server) { throw "Server is required. Use -Server or ONEC_SQL_SERVER." } if (-not $Database) { throw "Database is required. Use -Database or ONEC_SQL_DATABASE." } if (-not $User) { throw "User is required. Use -User or ONEC_SQL_USER." } if (-not $Password) { throw "Password is required. Use -Password or ONEC_SQL_PASSWORD." } if (-not $ReadResultPath) { throw "ReadResultPath is required." } if (-not $RouteIndexPath) { throw "RouteIndexPath is required." } if (-not $OutputPath) { throw "OutputPath is required." } function Test-HasProperty { param([object]$Object, [string]$Name) return $null -ne $Object -and ($Object.PSObject.Properties.Name -contains $Name) } function Convert-HexToBytes { param([string]$Hex) if (-not $Hex -or ($Hex.Length % 2) -ne 0) { return $null } $bytes = [byte[]]::new($Hex.Length / 2) for ($i = 0; $i -lt $bytes.Length; $i++) { $bytes[$i] = [Convert]::ToByte($Hex.Substring($i * 2, 2), 16) } return $bytes } function Convert-SqlValue { param([object]$Value) if ($null -eq $Value -or $Value -is [DBNull]) { return $null } if ($Value -is [byte[]]) { return [pscustomobject]@{ kind = "binary" length = $Value.Length hex = ([BitConverter]::ToString($Value) -replace "-", "").ToLowerInvariant() } } if ($Value -is [DateTime]) { return $Value.ToString("o") } return $Value } function Get-PhysicalTable { param([string]$Kind, [int]$SqlNumber) switch ($Kind) { "Catalog" { return "_Reference$SqlNumber" } "Document" { return "_Document$SqlNumber" } "Enum" { return "_Enum$SqlNumber" } default { return $null } } } function Get-StorageRole { param([string]$Kind) switch ($Kind) { "Catalog" { return "Reference" } "Document" { return "Document" } "Enum" { return "Enum" } default { return $null } } } function Get-ReferenceKind { param([string]$RefKind) switch ($RefKind) { "CatalogRef" { return "Catalog" } "DocumentRef" { return "Document" } "EnumRef" { return "Enum" } default { return $null } } } function Get-Columns { param( [System.Data.SqlClient.SqlConnection]$Connection, [string]$Table ) $command = $Connection.CreateCommand() $command.CommandText = @" SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @table ORDER BY ORDINAL_POSITION "@ $null = $command.Parameters.Add("@table", [System.Data.SqlDbType]::NVarChar, 128) $command.Parameters["@table"].Value = $Table $reader = $command.ExecuteReader() $columns = New-Object System.Collections.Generic.List[string] try { while ($reader.Read()) { $columns.Add([string]$reader.GetValue(0)) } } finally { $reader.Close() $command.Dispose() } return [string[]]$columns.ToArray() } function Quote-Identifier { param([string]$Name) return "[" + ($Name -replace "]", "]]") + "]" } function Select-PresentationColumns { param([string]$Kind, [string[]]$Columns) $wanted = New-Object System.Collections.Generic.List[string] foreach ($column in @("_IDRRef", "_Code", "_Description", "_Number", "_Date_Time", "_Posted", "_Marked", "_EnumOrder")) { if ($Columns -contains $column) { $wanted.Add($column) } } if ($wanted.Count -eq 0 -and ($Columns -contains "_IDRRef")) { $wanted.Add("_IDRRef") } return [string[]]$wanted.ToArray() } function Resolve-ReferenceRow { param( [System.Data.SqlClient.SqlConnection]$Connection, [string]$Table, [string[]]$Columns, [byte[]]$IdBytes ) if (-not ($Columns -contains "_IDRRef")) { return [pscustomobject]@{ found = $false; values = $null; reason = "target_table_without__IDRRef" } } $select = ($Columns | ForEach-Object { Quote-Identifier $_ }) -join ", " $command = $Connection.CreateCommand() $command.CommandText = "SELECT TOP (1) $select FROM $(Quote-Identifier $Table) WHERE [_IDRRef] = @id;" $null = $command.Parameters.Add("@id", [System.Data.SqlDbType]::Binary, 16) $command.Parameters["@id"].Value = $IdBytes $reader = $command.ExecuteReader() try { if (-not $reader.Read()) { return [pscustomobject]@{ found = $false; values = $null; reason = "not_found" } } $values = [ordered]@{} for ($i = 0; $i -lt $reader.FieldCount; $i++) { $values[$reader.GetName($i)] = Convert-SqlValue $reader.GetValue($i) } return [pscustomobject]@{ found = $true; values = [pscustomobject]$values; reason = $null } } finally { $reader.Close() $command.Dispose() } } function Find-IdTables { param( [System.Data.SqlClient.SqlConnection]$Connection, [string]$TableLike, [byte[]]$IdBytes ) $command = $Connection.CreateCommand() $command.CommandTimeout = 0 $command.CommandText = @" DECLARE @id binary(16) = @p; DECLARE @sql nvarchar(max) = N''; SELECT @sql = @sql + N'IF EXISTS (SELECT 1 FROM ' + QUOTENAME(TABLE_NAME) + N' WHERE [_IDRRef] = @id) SELECT N''' + TABLE_NAME + N''' AS table_name;' FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME = '_IDRRef' AND TABLE_NAME LIKE @table_like; EXEC sp_executesql @sql, N'@id binary(16)', @id=@id; "@ $null = $command.Parameters.Add("@p", [System.Data.SqlDbType]::Binary, 16) $command.Parameters["@p"].Value = $IdBytes $null = $command.Parameters.Add("@table_like", [System.Data.SqlDbType]::NVarChar, 128) $command.Parameters["@table_like"].Value = $TableLike $reader = $command.ExecuteReader() $tables = New-Object System.Collections.Generic.List[string] try { do { while ($reader.Read()) { $tables.Add([string]$reader.GetValue(0)) } } while ($reader.NextResult()) } finally { $reader.Close() $command.Dispose() } return [string[]]$tables.ToArray() } function Get-TableLikeForKind { param([string]$Kind) switch ($Kind) { "Catalog" { return "_Reference%" } "Document" { return "_Document%" } "Enum" { return "_Enum%" } default { return "%" } } } function Get-RouteByTable { param([hashtable]$TableIndex, [string]$Table) if ($TableIndex.ContainsKey($Table)) { return $TableIndex[$Table] } $match = [regex]::Match($Table, "^(_Reference|_Document|_Enum)(\d+)X\d+$") if ($match.Success) { $baseTable = "$($match.Groups[1].Value)$($match.Groups[2].Value)" if ($TableIndex.ContainsKey($baseTable)) { $route = $TableIndex[$baseTable] return [pscustomobject]@{ kind = $route.kind name = $route.name guid = $route.guid table = $Table base_table = $baseTable storage_role = $route.storage_role sql_number = $route.sql_number source = "extension_table_suffix" } } } return $null } function New-TargetIndex { param([object]$RouteIndex) $targets = @{} foreach ($entry in $RouteIndex.objects.PSObject.Properties) { $guid = [string]$entry.Name $object = $entry.Value foreach ($top in @($object.xml_top_objects)) { if (-not (Test-HasProperty $top "xml_kind") -or -not (Test-HasProperty $top "name")) { continue } $kind = [string]$top.xml_kind $name = [string]$top.name $storageRole = Get-StorageRole $kind if (-not $storageRole) { continue } $route = @($object.dbnames | Where-Object { $_.storage_role -eq $storageRole } | Select-Object -First 1) if ($route.Count -eq 0) { continue } $table = Get-PhysicalTable -Kind $kind -SqlNumber ([int]$route[0].sql_number) if (-not $table) { continue } $key = "$kind|$name" $isBaseConfig = (Test-HasProperty $top "relative_path") -and ([string]$top.relative_path -notlike "*\*\\*") $candidate = [pscustomobject]@{ kind = $kind name = $name guid = $guid synonym = if (Test-HasProperty $top "synonym") { $top.synonym } else { $null } relative_path = if (Test-HasProperty $top "relative_path") { $top.relative_path } else { $null } storage_role = $storageRole sql_number = [int]$route[0].sql_number table = $table source_file = $route[0].source_file base_config_score = if ($isBaseConfig) { 1 } else { 0 } } if (-not $targets.ContainsKey($key)) { $targets[$key] = $candidate } elseif ($candidate.base_config_score -gt $targets[$key].base_config_score) { $targets[$key] = $candidate } } } return $targets } function New-TableIndex { param([hashtable]$TargetIndex) $tables = @{} foreach ($entry in $TargetIndex.GetEnumerator()) { $target = $entry.Value if ($target.table -and -not $tables.ContainsKey($target.table)) { $tables[$target.table] = $target } } return $tables } function Add-ReferenceCells { param( [System.Collections.Generic.List[object]]$References, [object[]]$Rows, [string]$Scope, [string]$TablePartName, [string]$TablePartTable ) for ($rowIndex = 0; $rowIndex -lt $Rows.Count; $rowIndex++) { $row = $Rows[$rowIndex] foreach ($cellProperty in $row.PSObject.Properties) { $cell = $cellProperty.Value if (-not (Test-HasProperty $cell "value_type") -or -not $cell.value_type) { continue } if (-not (Test-HasProperty $cell.value_type "types")) { continue } $types = @($cell.value_type.types) if ($types.Count -ne 1) { continue } $typeName = [string]$types[0] $match = [regex]::Match($typeName, "^cfg:(CatalogRef|DocumentRef|EnumRef)\.(.+)$") if (-not $match.Success) { continue } if (-not (Test-HasProperty $cell "value") -or -not $cell.value) { continue } if (-not (Test-HasProperty $cell.value "kind") -or $cell.value.kind -ne "binary") { continue } if (-not (Test-HasProperty $cell.value "hex")) { continue } $hex = [string]$cell.value.hex if ($hex -eq "00000000000000000000000000000000") { continue } $References.Add([pscustomobject]@{ scope = $Scope table_part_name = $TablePartName table_part_table = $TablePartTable row_index = $rowIndex cell_key = $cellProperty.Name metadata_path = $cell.metadata_path metadata_name = $cell.metadata_name metadata_uuid = $cell.metadata_uuid column = $cell.column ref_type = $typeName ref_kind = $match.Groups[1].Value target_kind = Get-ReferenceKind $match.Groups[1].Value target_name = $match.Groups[2].Value id_hex = $hex }) } } } $readResult = Get-Content -LiteralPath $ReadResultPath -Raw -Encoding UTF8 | ConvertFrom-Json $routeIndex = Get-Content -LiteralPath $RouteIndexPath -Raw -Encoding UTF8 | ConvertFrom-Json $targetIndex = New-TargetIndex $routeIndex $tableIndex = New-TableIndex $targetIndex $references = New-Object System.Collections.Generic.List[object] Add-ReferenceCells -References $references -Rows @($readResult.main.rows) -Scope "main" -TablePartName $null -TablePartTable $readResult.main.table foreach ($part in @($readResult.table_parts)) { Add-ReferenceCells -References $references -Rows @($part.rows) -Scope "table_part" -TablePartName $part.name -TablePartTable $part.table } $connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;Application Name=Codex 1C SQL Reference Resolver;" $connection = [System.Data.SqlClient.SqlConnection]::new($connectionString) $tableColumnCache = @{} $resolved = New-Object System.Collections.Generic.List[object] $targetMisses = 0 $rowMisses = 0 $resolvedCount = 0 $alternateHitCount = 0 try { $connection.Open() foreach ($reference in [object[]]$references.ToArray()) { $targetKey = "$($reference.target_kind)|$($reference.target_name)" $target = if ($targetIndex.ContainsKey($targetKey)) { $targetIndex[$targetKey] } else { $null } if (-not $target) { $targetMisses += 1 $resolved.Add([pscustomobject]@{ reference = $reference target = $null found = $false reason = "target_metadata_not_found" values = $null }) continue } if (-not $tableColumnCache.ContainsKey($target.table)) { $tableColumnCache[$target.table] = Get-Columns -Connection $connection -Table $target.table } $columns = Select-PresentationColumns -Kind $target.kind -Columns $tableColumnCache[$target.table] $idBytes = Convert-HexToBytes $reference.id_hex $row = Resolve-ReferenceRow -Connection $connection -Table $target.table -Columns $columns -IdBytes $idBytes $alternateHits = @() if (-not $row.found) { $hitTables = Find-IdTables -Connection $connection -TableLike (Get-TableLikeForKind $target.kind) -IdBytes $idBytes $alternateRows = New-Object System.Collections.Generic.List[object] foreach ($hitTable in @($hitTables | Where-Object { $_ -ne $target.table })) { if (-not $tableColumnCache.ContainsKey($hitTable)) { $tableColumnCache[$hitTable] = Get-Columns -Connection $connection -Table $hitTable } $hitColumns = Select-PresentationColumns -Kind $target.kind -Columns $tableColumnCache[$hitTable] $hitRow = Resolve-ReferenceRow -Connection $connection -Table $hitTable -Columns $hitColumns -IdBytes $idBytes $alternateRows.Add([pscustomobject]@{ table = $hitTable route = Get-RouteByTable -TableIndex $tableIndex -Table $hitTable found = $hitRow.found values = $hitRow.values }) } $alternateHits = [object[]]$alternateRows.ToArray() $alternateHitCount += $alternateHits.Count } if ($row.found) { $resolvedCount += 1 } else { $rowMisses += 1 } $resolved.Add([pscustomobject]@{ reference = $reference target = $target found = $row.found reason = $row.reason values = $row.values alternate_hits = $alternateHits }) } $result = [ordered]@{ schema = "onec_sql_reference_resolution.v1" read_result_path = (Resolve-Path -LiteralPath $ReadResultPath).ProviderPath route_index_path = (Resolve-Path -LiteralPath $RouteIndexPath).ProviderPath server = $Server database = $Database source = [pscustomobject]@{ kind = $readResult.kind identity = $readResult.identity } summary = [pscustomobject]@{ reference_cells = $references.Count resolved = $resolvedCount target_metadata_misses = $targetMisses row_misses = $rowMisses alternate_hits = $alternateHitCount } references = [object[]]$resolved.ToArray() } $outputDirectory = Split-Path -Parent $OutputPath if ($outputDirectory) { New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null } $result | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 [pscustomobject]@{ output = (Resolve-Path -LiteralPath $OutputPath).ProviderPath reference_cells = $references.Count resolved = $resolvedCount target_metadata_misses = $targetMisses row_misses = $rowMisses alternate_hits = $alternateHitCount } | ConvertTo-Json -Depth 10 } finally { $connection.Close() $connection.Dispose() }