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]$BaseMetadataDir = "reports\1c-sql\upo\structured-metadata-all-kinds", [string]$ExtensionGuidIndex = "reports\1c-sql\upo\xml-guid-index-extensions.json", [string]$Output ) 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." } function U { param([string]$Base64) return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Base64)) } $ruExtension = U "0KDQsNGB0YjQuNGA0LXQvdC40LU=" $ruConfigObject = U "0J7QsdGK0LXQutGC0JrQvtC90YTQuNCz0YPRgNCw0YbQuNC4" $ruForm = U "0KTQvtGA0LzQsA==" $kindRu = @{ Configuration = (U "0JrQvtC90YTQuNCz0YPRgNCw0YbQuNGP") CommonModule = (U "0J7QsdGJ0LjQudCc0L7QtNGD0LvRjA==") CommonForm = (U "0J7QsdGJ0LDRj9Ck0L7RgNC80LA=") CommonCommand = (U "0J7QsdGJ0LDRj9Ca0L7QvNCw0L3QtNCw") CommonAttribute = (U "0J7QsdGJ0LjQudCg0LXQutCy0LjQt9C40YI=") CommonTemplate = (U "0J7QsdGJ0LjQudCc0LDQutC10YI=") CommonPicture = (U "0J7QsdGJ0LDRj9Ca0LDRgNGC0LjQvdC60LA=") Catalog = (U "0KHQv9GA0LDQstC+0YfQvdC40Lo=") Document = (U "0JTQvtC60YPQvNC10L3Rgg==") DataProcessor = (U "0J7QsdGA0LDQsdC+0YLQutCw") Report = (U "0J7RgtGH0LXRgg==") InformationRegister = (U "0KDQtdCz0LjRgdGC0YDQodCy0LXQtNC10L3QuNC5") AccumulationRegister = (U "0KDQtdCz0LjRgdGC0YDQndCw0LrQvtC/0LvQtdC90LjRjw==") AccountingRegister = (U "0KDQtdCz0LjRgdGC0YDQkdGD0YXQs9Cw0LvRgtC10YDQuNC4") CalculationRegister = (U "0KDQtdCz0LjRgdGC0YDQoNCw0YHRh9C10YLQsA==") Enum = (U "0J/QtdGA0LXRh9C40YHQu9C10L3QuNC1") Form = $ruForm Template = (U "0JzQsNC60LXRgg==") Role = (U "0KDQvtC70Yw=") Subsystem = (U "0J/QvtC00YHQuNGB0YLQtdC80LA=") ExchangePlan = (U "0J/Qu9Cw0L3QntCx0LzQtdC90LA=") BusinessProcess = (U "0JHQuNC30L3QtdGB0J/RgNC+0YbQtdGB0YE=") Task = (U "0JfQsNC00LDRh9Cw") Constant = (U "0JrQvtC90YHRgtCw0L3RgtCw") ChartOfCharacteristicTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KXQsNGA0LDQutGC0LXRgNC40YHRgtC40Lo=") ChartOfAccounts = (U "0J/Qu9Cw0L3QodGH0LXRgtC+0LI=") ChartOfCalculationTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KDQsNGB0YfQtdGC0LA=") } $folderKind = @{ CommonModules = "CommonModule" CommonForms = "CommonForm" CommonCommands = "CommonCommand" CommonAttributes = "CommonAttribute" CommonTemplates = "CommonTemplate" CommonPictures = "CommonPicture" Catalogs = "Catalog" Documents = "Document" DataProcessors = "DataProcessor" Reports = "Report" InformationRegisters = "InformationRegister" AccumulationRegisters = "AccumulationRegister" AccountingRegisters = "AccountingRegister" CalculationRegisters = "CalculationRegister" Enums = "Enum" Forms = "Form" Templates = "Template" Roles = "Role" Subsystems = "Subsystem" ExchangePlans = "ExchangePlan" BusinessProcesses = "BusinessProcess" Tasks = "Task" Constants = "Constant" ChartsOfCharacteristicTypes = "ChartOfCharacteristicTypes" ChartsOfAccounts = "ChartOfAccounts" ChartsOfCalculationTypes = "ChartOfCalculationTypes" } function Convert-RefBytesToGuid { param([byte[]]$Bytes) if ($Bytes.Length -ne 16) { return $null } $hex = ($Bytes | ForEach-Object { $_.ToString("x2") }) -join "" return "{0}-{1}-{2}-{3}-{4}" -f $hex.Substring(24, 8), $hex.Substring(20, 4), $hex.Substring(16, 4), $hex.Substring(0, 4), $hex.Substring(4, 12) } function Convert-ToSha256Hex { param([byte[]]$Bytes) $sha = [System.Security.Cryptography.SHA256]::Create() try { return (($sha.ComputeHash($Bytes) | ForEach-Object { $_.ToString("x2") }) -join "") } finally { $sha.Dispose() } } function Join-ByteArrays { param([object[]]$Rows) $ordered = @($Rows | Sort-Object part_no) $total = 0 foreach ($row in $ordered) { $total += $row.bytes.Length } $buffer = [byte[]]::new($total) $offset = 0 foreach ($row in $ordered) { [Array]::Copy($row.bytes, 0, $buffer, $offset, $row.bytes.Length) $offset += $row.bytes.Length } return $buffer } function New-Connection { $connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Application Name=Codex 1C Saved State Object Compare;" $connection = [System.Data.SqlClient.SqlConnection]::new($connectionString) $connection.Open() return $connection } function Read-TableFiles { param( [System.Data.SqlClient.SqlConnection]$Connection, [string]$Table, [string[]]$FileNames ) $command = $Connection.CreateCommand() if ($FileNames -and $FileNames.Count -gt 0) { $placeholders = @() for ($i = 0; $i -lt $FileNames.Count; $i++) { $paramName = "@p$i" $placeholders += $paramName $null = $command.Parameters.Add($paramName, [System.Data.SqlDbType]::NVarChar, 512) $command.Parameters[$paramName].Value = $FileNames[$i] } $command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] WHERE FileName IN ($($placeholders -join ', ')) ORDER BY FileName, PartNo" } else { $command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] ORDER BY FileName, PartNo" } $reader = $null $groups = @{} try { $reader = $command.ExecuteReader() while ($reader.Read()) { $fileName = [string]$reader.GetValue(0) if (-not $groups.ContainsKey($fileName)) { $groups[$fileName] = @() } $groups[$fileName] += [pscustomobject]@{ part_no = [int]$reader.GetValue(1) bytes = [byte[]]$reader.GetValue(2) } } } finally { if ($reader -ne $null) { $reader.Dispose() } if ($command -ne $null) { $command.Dispose() } } $result = @{} foreach ($fileName in $groups.Keys) { $bytes = Join-ByteArrays @($groups[$fileName]) $result[$fileName] = [pscustomobject]@{ file_name = $fileName chunks = @($groups[$fileName]).Count bytes = $bytes.Length sha256 = Convert-ToSha256Hex $bytes } } return $result } function Read-Extensions { param([System.Data.SqlClient.SqlConnection]$Connection) $command = $Connection.CreateCommand() $command.CommandText = "SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionUsePurpose], [_ExtensionScope] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName]" $reader = $null $map = @{} try { $reader = $command.ExecuteReader() while ($reader.Read()) { $bytes = [byte[]]$reader.GetValue(0) $guid = Convert-RefBytesToGuid $bytes $map[$guid] = [pscustomobject]@{ guid = $guid name = [string]$reader.GetValue(1) order = [decimal]$reader.GetValue(2) use_purpose = [decimal]$reader.GetValue(3) scope = [decimal]$reader.GetValue(4) } } } finally { if ($reader -ne $null) { $reader.Dispose() } if ($command -ne $null) { $command.Dispose() } } return $map } function Get-BaseObjectInfo { param([string]$Guid) $metadataRoot = [System.IO.Path]::GetFullPath($BaseMetadataDir) if (-not (Test-Path -LiteralPath $metadataRoot)) { return $null } $match = Get-ChildItem -LiteralPath $metadataRoot -Recurse -Filter *.json -File | Select-String -SimpleMatch $Guid -List | Select-Object -First 1 if (-not $match) { return $null } $data = Get-Content -LiteralPath $match.Path -Raw -Encoding UTF8 | ConvertFrom-Json $kind = [string]$data.kind $name = [string]$data.identity.name $synonym = $null if ($data.identity.synonyms -and $data.identity.synonyms.ru) { $synonym = [string]$data.identity.synonyms.ru } return [pscustomobject]@{ layer = "base" kind = $kind kind_ru = if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind } name = $name synonym = $synonym full_name = "$(if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }).$name" guid = $Guid evidence = [pscustomobject]@{ metadata_file = $match.Path xml_file = $data.xml_file } } } function Get-ExtensionGuidMap { $path = [System.IO.Path]::GetFullPath($ExtensionGuidIndex) if (-not (Test-Path -LiteralPath $path)) { return @{} } $data = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json return $data.guid_map } function Get-ExtensionObjectInfo { param( [string]$ExtensionName, [string]$ObjectGuid, [object]$GuidMap ) $entry = $GuidMap.$ObjectGuid $top = $null if ($entry -and $entry.top_objects -and @($entry.top_objects).Count -gt 0) { $top = @($entry.top_objects)[0] } $relativePath = if ($top) { [string]$top.relative_path } else { $null } $parts = if ($relativePath) { $relativePath -split "[\\/]" } else { @() } $parentKind = $null $parentName = $null $artifactKind = if ($top) { [string]$top.xml_kind } else { "ConfigCASObject" } $artifactName = if ($top) { [string]$top.name } else { $ObjectGuid } $synonym = if ($top -and $top.synonym) { [string]$top.synonym } else { $null } for ($i = 0; $i -lt $parts.Count - 1; $i++) { if ($folderKind.ContainsKey($parts[$i]) -and $parts[$i] -ne "Forms" -and $parts[$i] -ne "Templates") { $parentKind = $folderKind[$parts[$i]] if ($i + 1 -lt $parts.Count) { $parentName = $parts[$i + 1] } break } } $artifactKindRu = if ($kindRu.ContainsKey($artifactKind)) { $kindRu[$artifactKind] } else { $artifactKind } $parentKindRu = if ($parentKind -and $kindRu.ContainsKey($parentKind)) { $kindRu[$parentKind] } else { $parentKind } $fullName = if ($parentKind -and $artifactKind -eq "Form") { "$ruExtension.$ExtensionName.$parentKindRu.$parentName.$ruForm.$artifactName" } elseif ($parentKind) { "$ruExtension.$ExtensionName.$parentKindRu.$parentName.$artifactKindRu.$artifactName" } else { "$ruExtension.$ExtensionName.$artifactKindRu.$artifactName" } return [pscustomobject]@{ layer = "extension" extension = $ExtensionName parent_kind = $parentKind parent_kind_ru = $parentKindRu parent_name = $parentName kind = $artifactKind kind_ru = $artifactKindRu name = $artifactName synonym = $synonym full_name = $fullName guid = $ObjectGuid relative_path = $relativePath evidence = [pscustomobject]@{ xml_path = if ($top) { [string]$top.path } else { $null } } } } function Split-ObjectPart { param([string]$FileName) if ($FileName -match '^([0-9a-fA-F-]{36})(\..+)?$') { return [pscustomobject]@{ object_id = $Matches[1].ToLowerInvariant(); suffix = if ($Matches[2]) { $Matches[2] } else { "" } } } return $null } function Split-ExtensionFileName { param([string]$FileName) if ($FileName -match '^([0-9a-fA-F-]{36})__(.+)$') { $rest = $Matches[2] if ($rest -eq "configinfo") { return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $null; suffix = ""; system_name = "configinfo" } } $part = Split-ObjectPart $rest return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $part.object_id; suffix = $part.suffix; system_name = $null } } return $null } function New-StorageEvidence { param( [string]$SavedTable, [string]$ActiveTable, [string]$FileName, [object]$Saved, [object]$Active ) return [pscustomobject]@{ saved_table = $SavedTable active_table = $ActiveTable file_name = $FileName saved_bytes = $Saved.bytes active_bytes = if ($Active) { $Active.bytes } else { $null } saved_sha256 = $Saved.sha256 active_sha256 = if ($Active) { $Active.sha256 } else { $null } active_exists = [bool]$Active changed = (-not $Active) -or ($Saved.sha256 -ne $Active.sha256) -or ($Saved.bytes -ne $Active.bytes) } } $connection = New-Connection try { $configSave = Read-TableFiles $connection "ConfigSave" $null $configCassave = Read-TableFiles $connection "ConfigCASSave" $null $configActive = Read-TableFiles $connection "Config" @($configSave.Keys) $configCasActive = Read-TableFiles $connection "ConfigCAS" @($configCassave.Keys) $extensions = Read-Extensions $connection } finally { if ($connection -ne $null) { $connection.Dispose() } } $objectChangesByKey = @{} $systemChanges = @() foreach ($fileName in $configSave.Keys) { $saved = $configSave[$fileName] $active = if ($configActive.ContainsKey($fileName)) { $configActive[$fileName] } else { $null } $evidence = New-StorageEvidence "ConfigSave" "Config" $fileName $saved $active if (-not $evidence.changed) { continue } $part = Split-ObjectPart $fileName if (-not $part) { $systemChanges += [pscustomobject]@{ layer = "base"; name = $fileName; storage = $evidence } continue } $info = Get-BaseObjectInfo $part.object_id if (-not $info) { $info = [pscustomobject]@{ layer = "base" kind = "ConfigObject" kind_ru = $ruConfigObject name = $part.object_id synonym = $null full_name = "$ruConfigObject.$($part.object_id)" guid = $part.object_id evidence = $null } } $key = "base::$($part.object_id)" if (-not $objectChangesByKey.ContainsKey($key)) { $objectChangesByKey[$key] = [ordered]@{ layer = $info.layer kind = $info.kind kind_ru = $info.kind_ru name = $info.name synonym = $info.synonym full_name = $info.full_name guid = $info.guid change_state = "saved_not_applied" storage = @() evidence = $info.evidence } } $objectChangesByKey[$key].storage += $evidence } $extensionGuidMap = Get-ExtensionGuidMap foreach ($fileName in $configCassave.Keys) { $saved = $configCassave[$fileName] $active = if ($configCasActive.ContainsKey($fileName)) { $configCasActive[$fileName] } else { $null } $evidence = New-StorageEvidence "ConfigCASSave" "ConfigCAS" $fileName $saved $active if (-not $evidence.changed) { continue } $part = Split-ExtensionFileName $fileName if (-not $part) { $systemChanges += [pscustomobject]@{ layer = "extension"; name = $fileName; storage = $evidence } continue } $extension = if ($extensions.ContainsKey($part.extension_id)) { $extensions[$part.extension_id] } else { $null } $extensionName = if ($extension) { $extension.name } else { $part.extension_id } if ($part.system_name) { $systemChanges += [pscustomobject]@{ layer = "extension"; extension = $extensionName; name = $part.system_name; storage = $evidence } continue } $info = Get-ExtensionObjectInfo $extensionName $part.object_id $extensionGuidMap $key = "extension::$($part.extension_id)::$($part.object_id)" if (-not $objectChangesByKey.ContainsKey($key)) { $objectChangesByKey[$key] = [ordered]@{ layer = $info.layer extension = $info.extension parent_kind = $info.parent_kind parent_kind_ru = $info.parent_kind_ru parent_name = $info.parent_name kind = $info.kind kind_ru = $info.kind_ru name = $info.name synonym = $info.synonym full_name = $info.full_name guid = $info.guid relative_path = $info.relative_path change_state = "saved_not_applied" storage = @() evidence = $info.evidence } } $objectChangesByKey[$key].storage += $evidence } $objectChanges = @($objectChangesByKey.Values | ForEach-Object { [pscustomobject]$_ } | Sort-Object layer, extension, full_name) $result = [pscustomobject]@{ schema = "onec_saved_state_object_comparison.v1" server = $Server database = $Database view = "saved_not_applied_vs_active" object_changes = $objectChanges system_changes = $systemChanges counts = [pscustomobject]@{ object_changes = $objectChanges.Count system_changes = $systemChanges.Count config_save_files = $configSave.Count config_cas_save_files = $configCassave.Count } safety = [pscustomobject]@{ read_only = $true sql_write_performed = $false exposes_storage_evidence = $true public_terms_are_1c_objects = $true } } $json = $result | ConvertTo-Json -Depth 20 if ($Output) { $parent = Split-Path -Parent $Output if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $json | Set-Content -LiteralPath $Output -Encoding UTF8 } $json