Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user